From f99cd8f3e29caf194352a1a9c4b38b0f3e6d02b4 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Thu, 19 Jan 2023 14:02:18 +0530 Subject: [PATCH 001/106] changes storage layer to take json instead of config file path --- .../supertokens/storage/postgresql/Start.java | 87 +++++++++++-------- .../storage/postgresql/config/Config.java | 22 ++--- .../postgresql/config/PostgreSQLConfig.java | 10 +-- 3 files changed, 67 insertions(+), 52 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 5df133d0..7ad44d62 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -35,6 +35,7 @@ import io.supertokens.pluginInterface.emailverification.EmailVerificationTokenInfo; import io.supertokens.pluginInterface.emailverification.exception.DuplicateEmailVerificationTokenException; import io.supertokens.pluginInterface.emailverification.sqlStorage.EmailVerificationSQLStorage; +import io.supertokens.pluginInterface.exceptions.InvalidConfigException; import io.supertokens.pluginInterface.exceptions.QuitProgramFromPluginException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; @@ -119,8 +120,8 @@ public STORAGE_TYPE getType() { } @Override - public void loadConfig(String configFilePath, Set logLevels) { - Config.loadConfig(this, configFilePath, logLevels); + public void loadConfig(JsonObject configJson, Set logLevels) throws InvalidConfigException { + Config.loadConfig(this, configJson, logLevels); } @Override @@ -247,21 +248,21 @@ private T startTransactionHelper(TransactionLogic logic, TransactionIsola defaultTransactionIsolation = con.getTransactionIsolation(); int libIsolationLevel = Connection.TRANSACTION_SERIALIZABLE; switch (isolationLevel) { - case SERIALIZABLE: - libIsolationLevel = Connection.TRANSACTION_SERIALIZABLE; - break; - case REPEATABLE_READ: - libIsolationLevel = Connection.TRANSACTION_REPEATABLE_READ; - break; - case READ_COMMITTED: - libIsolationLevel = Connection.TRANSACTION_READ_COMMITTED; - break; - case READ_UNCOMMITTED: - libIsolationLevel = Connection.TRANSACTION_READ_UNCOMMITTED; - break; - case NONE: - libIsolationLevel = Connection.TRANSACTION_NONE; - break; + case SERIALIZABLE: + libIsolationLevel = Connection.TRANSACTION_SERIALIZABLE; + break; + case REPEATABLE_READ: + libIsolationLevel = Connection.TRANSACTION_REPEATABLE_READ; + break; + case READ_COMMITTED: + libIsolationLevel = Connection.TRANSACTION_READ_COMMITTED; + break; + case READ_UNCOMMITTED: + libIsolationLevel = Connection.TRANSACTION_READ_UNCOMMITTED; + break; + case NONE: + libIsolationLevel = Connection.TRANSACTION_NONE; + break; } con.setTransactionIsolation(libIsolationLevel); con.setAutoCommit(false); @@ -383,7 +384,8 @@ public void close() { @Override public void createNewSession(String sessionHandle, String userId, String refreshTokenHash2, - JsonObject userDataInDatabase, long expiry, JsonObject userDataInJWT, long createdAtTime) + JsonObject userDataInDatabase, long expiry, JsonObject userDataInJWT, + long createdAtTime) throws StorageQueryException { try { SessionQueries.createNewSession(this, sessionHandle, userId, refreshTokenHash2, userDataInDatabase, expiry, @@ -493,7 +495,7 @@ public SessionInfo getSessionInfo_Transaction(TransactionConnection con, String @Override public void updateSessionInfo_Transaction(TransactionConnection con, String sessionHandle, String refreshTokenHash2, - long expiry) throws StorageQueryException { + long expiry) throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { SessionQueries.updateSessionInfo_Transaction(this, sqlCon, sessionHandle, refreshTokenHash2, expiry); @@ -544,8 +546,8 @@ void handleKillSignalForWhenItHappens() { } @Override - public boolean canBeUsed(String configFilePath) { - return Config.canBeUsed(configFilePath); + public boolean canBeUsed(JsonObject configJson) { + return Config.canBeUsed(configJson); } @Override @@ -735,7 +737,8 @@ public PasswordResetTokenInfo[] getAllPasswordResetTokenInfoForUser(String userI @Override public PasswordResetTokenInfo[] getAllPasswordResetTokenInfoForUser_Transaction(TransactionConnection con, - String userId) throws StorageQueryException { + String userId) + throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { return EmailPasswordQueries.getAllPasswordResetTokenInfoForUser_Transaction(this, sqlCon, userId); @@ -810,7 +813,8 @@ public void deleteExpiredEmailVerificationTokens() throws StorageQueryException @Override public EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUser_Transaction(TransactionConnection con, - String userId, String email) throws StorageQueryException { + String userId, String email) + throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { return EmailVerificationQueries.getAllEmailVerificationTokenInfoForUser_Transaction(this, sqlCon, userId, @@ -822,7 +826,7 @@ public EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUser_Tran @Override public void deleteAllEmailVerificationTokensForUser_Transaction(TransactionConnection con, String userId, - String email) throws StorageQueryException { + String email) throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { EmailVerificationQueries.deleteAllEmailVerificationTokensForUser_Transaction(this, sqlCon, userId, email); @@ -833,7 +837,7 @@ public void deleteAllEmailVerificationTokensForUser_Transaction(TransactionConne @Override public void updateIsEmailVerified_Transaction(TransactionConnection con, String userId, String email, - boolean isEmailVerified) throws StorageQueryException { + boolean isEmailVerified) throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { EmailVerificationQueries.updateUsersIsEmailVerified_Transaction(this, sqlCon, userId, email, @@ -936,7 +940,7 @@ public boolean isEmailVerified(String userId, String email) throws StorageQueryE @Override @Deprecated public UserInfo[] getUsers(@Nonnull String userId, @Nonnull Long timeJoined, @Nonnull Integer limit, - @Nonnull String timeJoinedOrder) throws StorageQueryException { + @Nonnull String timeJoinedOrder) throws StorageQueryException { try { return EmailPasswordQueries.getUsersInfo(this, userId, timeJoined, limit, timeJoinedOrder); } catch (SQLException e) { @@ -975,7 +979,9 @@ public void deleteExpiredPasswordResetTokens() throws StorageQueryException { @Override public io.supertokens.pluginInterface.thirdparty.UserInfo getUserInfoUsingId_Transaction(TransactionConnection con, - String thirdPartyId, String thirdPartyUserId) throws StorageQueryException { + String thirdPartyId, + String thirdPartyUserId) + throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { return ThirdPartyQueries.getUserInfoUsingId_Transaction(this, sqlCon, thirdPartyId, thirdPartyUserId); @@ -986,7 +992,7 @@ public io.supertokens.pluginInterface.thirdparty.UserInfo getUserInfoUsingId_Tra @Override public void updateUserEmail_Transaction(TransactionConnection con, String thirdPartyId, String thirdPartyUserId, - String newEmail) throws StorageQueryException { + String newEmail) throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { ThirdPartyQueries.updateUserEmail_Transaction(this, sqlCon, thirdPartyId, thirdPartyUserId, newEmail); @@ -1037,7 +1043,8 @@ public void deleteThirdPartyUser(String userId) throws StorageQueryException { @Override public io.supertokens.pluginInterface.thirdparty.UserInfo getThirdPartyUserInfoUsingId(String thirdPartyId, - String thirdPartyUserId) throws StorageQueryException { + String thirdPartyUserId) + throws StorageQueryException { try { return ThirdPartyQueries.getThirdPartyUserInfoUsingId(this, thirdPartyId, thirdPartyUserId); } catch (SQLException e) { @@ -1058,7 +1065,9 @@ public io.supertokens.pluginInterface.thirdparty.UserInfo getThirdPartyUserInfoU @Override @Deprecated public io.supertokens.pluginInterface.thirdparty.UserInfo[] getThirdPartyUsers(@NotNull String userId, - @NotNull Long timeJoined, @NotNull Integer limit, @NotNull String timeJoinedOrder) + @NotNull Long timeJoined, + @NotNull Integer limit, + @NotNull String timeJoinedOrder) throws StorageQueryException { try { return ThirdPartyQueries.getThirdPartyUsers(this, userId, timeJoined, limit, timeJoinedOrder); @@ -1070,7 +1079,8 @@ public io.supertokens.pluginInterface.thirdparty.UserInfo[] getThirdPartyUsers(@ @Override @Deprecated public io.supertokens.pluginInterface.thirdparty.UserInfo[] getThirdPartyUsers(@NotNull Integer limit, - @NotNull String timeJoinedOrder) throws StorageQueryException { + @NotNull String timeJoinedOrder) + throws StorageQueryException { try { return ThirdPartyQueries.getThirdPartyUsers(this, limit, timeJoinedOrder); } catch (SQLException e) { @@ -1109,7 +1119,8 @@ public long getUsersCount(RECIPE_ID[] includeRecipeIds) throws StorageQueryExcep @Override public AuthRecipeUserInfo[] getUsers(@NotNull Integer limit, @NotNull String timeJoinedOrder, - @Nullable RECIPE_ID[] includeRecipeIds, @Nullable String userId, @Nullable Long timeJoined) + @Nullable RECIPE_ID[] includeRecipeIds, @Nullable String userId, + @Nullable Long timeJoined) throws StorageQueryException { try { return GeneralQueries.getUsers(this, limit, timeJoinedOrder, includeRecipeIds, userId, timeJoined); @@ -1314,7 +1325,8 @@ public void updateUserPhoneNumber_Transaction(TransactionConnection con, String @Override public void createDeviceWithCode(@Nullable String email, @Nullable String phoneNumber, @NotNull String linkCodeSalt, - PasswordlessCode code) throws StorageQueryException, DuplicateDeviceIdHashException, + PasswordlessCode code) + throws StorageQueryException, DuplicateDeviceIdHashException, DuplicateCodeIdException, DuplicateLinkCodeHashException { if (email == null && phoneNumber == null) { throw new IllegalArgumentException("Both email and phoneNumber can't be null"); @@ -1387,7 +1399,7 @@ public void createUser(io.supertokens.pluginInterface.passwordless.UserInfo user if (isPrimaryKeyError(((PSQLException) actualException).getServerErrorMessage(), Config.getConfig(this).getPasswordlessUsersTable()) || isPrimaryKeyError(((PSQLException) actualException).getServerErrorMessage(), - Config.getConfig(this).getUsersTable())) { + Config.getConfig(this).getUsersTable())) { throw new DuplicateUserIdException(); } @@ -1668,7 +1680,8 @@ public boolean createNewRoleOrDoNothingIfExists_Transaction(TransactionConnectio @Override public void addPermissionToRoleOrDoNothingIfExists_Transaction(TransactionConnection con, String role, - String permission) throws StorageQueryException, UnknownRoleException { + String permission) + throws StorageQueryException, UnknownRoleException { Connection sqlCon = (Connection) con.getConnection(); try { UserRolesQueries.addPermissionToRoleOrDoNothingIfExists_Transaction(this, sqlCon, role, permission); @@ -1720,7 +1733,7 @@ public boolean doesRoleExist_Transaction(TransactionConnection con, String role) @Override public void createUserIdMapping(String superTokensUserId, String externalUserId, - @Nullable String externalUserIdInfo) + @Nullable String externalUserIdInfo) throws StorageQueryException, UnknownSuperTokensUserIdException, UserIdMappingAlreadyExistsException { try { UserIdMappingQueries.createUserIdMapping(this, superTokensUserId, externalUserId, externalUserIdInfo); @@ -1789,7 +1802,7 @@ public UserIdMapping[] getUserIdMapping(String userId) throws StorageQueryExcept @Override public boolean updateOrDeleteExternalUserIdInfo(String userId, boolean isSuperTokensUserId, - @Nullable String externalUserIdInfo) throws StorageQueryException { + @Nullable String externalUserIdInfo) throws StorageQueryException { try { if (isSuperTokensUserId) { diff --git a/src/main/java/io/supertokens/storage/postgresql/config/Config.java b/src/main/java/io/supertokens/storage/postgresql/config/Config.java index 81039b9e..7e2d8293 100644 --- a/src/main/java/io/supertokens/storage/postgresql/config/Config.java +++ b/src/main/java/io/supertokens/storage/postgresql/config/Config.java @@ -19,13 +19,14 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.google.gson.JsonObject; import io.supertokens.pluginInterface.LOG_LEVEL; +import io.supertokens.pluginInterface.exceptions.InvalidConfigException; import io.supertokens.pluginInterface.exceptions.QuitProgramFromPluginException; import io.supertokens.storage.postgresql.ResourceDistributor; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.output.Logging; -import java.io.File; import java.io.IOException; import java.util.Set; @@ -36,11 +37,11 @@ public class Config extends ResourceDistributor.SingletonResource { private final Start start; private final Set logLevels; - private Config(Start start, String configFilePath, Set logLevels) { + private Config(Start start, JsonObject configJson, Set logLevels) throws InvalidConfigException { this.start = start; this.logLevels = logLevels; try { - config = loadPostgreSQLConfig(configFilePath); + config = loadPostgreSQLConfig(configJson); } catch (IOException e) { throw new QuitProgramFromPluginException(e); } @@ -50,11 +51,12 @@ private static Config getInstance(Start start) { return (Config) start.getResourceDistributor().getResource(RESOURCE_KEY); } - public static void loadConfig(Start start, String configFilePath, Set logLevels) { + public static void loadConfig(Start start, JsonObject configJson, Set logLevels) + throws InvalidConfigException { if (getInstance(start) != null) { return; } - start.getResourceDistributor().setResource(RESOURCE_KEY, new Config(start, configFilePath, logLevels)); + start.getResourceDistributor().setResource(RESOURCE_KEY, new Config(start, configJson, logLevels)); Logging.info(start, "Loading PostgreSQL config.", true); } @@ -69,17 +71,17 @@ public static Set getLogLevels(Start start) { return getInstance(start).logLevels; } - private PostgreSQLConfig loadPostgreSQLConfig(String configFilePath) throws IOException { + private PostgreSQLConfig loadPostgreSQLConfig(JsonObject configJson) throws IOException, InvalidConfigException { final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); - PostgreSQLConfig config = mapper.readValue(new File(configFilePath), PostgreSQLConfig.class); - config.validateAndInitialise(); + PostgreSQLConfig config = mapper.readValue(configJson.toString(), PostgreSQLConfig.class); + config.validate(); return config; } - public static boolean canBeUsed(String configFilePath) { + public static boolean canBeUsed(JsonObject configJson) { try { final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); - PostgreSQLConfig config = mapper.readValue(new File(configFilePath), PostgreSQLConfig.class); + PostgreSQLConfig config = mapper.readValue(configJson.toString(), PostgreSQLConfig.class); return config.getUser() != null || config.getPassword() != null || config.getConnectionURI() != null; } catch (Exception e) { return false; diff --git a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java index d4f5937e..a6e13074 100644 --- a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java +++ b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java @@ -19,7 +19,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; -import io.supertokens.pluginInterface.exceptions.QuitProgramFromPluginException; +import io.supertokens.pluginInterface.exceptions.InvalidConfigException; import java.net.URI; @@ -313,25 +313,25 @@ private String addSchemaToTableName(String tableName) { return name; } - void validateAndInitialise() { + void validate() throws InvalidConfigException { if (postgresql_connection_uri != null) { try { URI ignored = URI.create(postgresql_connection_uri); } catch (Exception e) { - throw new QuitProgramFromPluginException( + throw new InvalidConfigException( "The provided postgresql connection URI has an incorrect format. Please use a format like " + "postgresql://[user[:[password]]@]host[:port][/dbname][?attr1=val1&attr2=val2..."); } } else { if (this.getUser() == null) { - throw new QuitProgramFromPluginException( + throw new InvalidConfigException( "'postgresql_user' and 'postgresql_connection_uri' are not set. Please set at least one of " + "these values"); } } if (getConnectionPoolSize() <= 0) { - throw new QuitProgramFromPluginException( + throw new InvalidConfigException( "'postgresql_connection_pool_size' in the config.yaml file must be > 0"); } } From e16d29ba5d8086998a8025d603b372c3b078577f Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Thu, 19 Jan 2023 15:46:54 +0530 Subject: [PATCH 002/106] adds new functions skeleton --- .../java/io/supertokens/storage/postgresql/Start.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 7ad44d62..a7302b1c 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -124,6 +124,17 @@ public void loadConfig(JsonObject configJson, Set logLevels) throws I Config.loadConfig(this, configJson, logLevels); } + @Override + public String getUserPoolId(JsonObject jsonConfig) { + // TODO.. + return null; + } + + @Override + public void assertThatConfigFromSameUserPoolIsNotConflicting(JsonObject otherConfig) throws InvalidConfigException { + // TODO.. + } + @Override public void initFileLogging(String infoLogPath, String errorLogPath) { Logging.initFileLogging(this, infoLogPath, errorLogPath); From 841e84e061dd058a91c8058417ed974b97ddedb3 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Thu, 19 Jan 2023 22:16:44 +0530 Subject: [PATCH 003/106] adds checks for conflicting configs for user pools --- .../supertokens/storage/postgresql/Start.java | 7 +-- .../storage/postgresql/config/Config.java | 22 ++++++++ .../postgresql/config/PostgreSQLConfig.java | 55 ++++++++++++++++++- 3 files changed, 78 insertions(+), 6 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index a7302b1c..dbf061cd 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -125,14 +125,13 @@ public void loadConfig(JsonObject configJson, Set logLevels) throws I } @Override - public String getUserPoolId(JsonObject jsonConfig) { - // TODO.. - return null; + public String getUserPoolId(JsonObject jsonConfig) throws InvalidConfigException { + return Config.getUserPoolId(this, jsonConfig); } @Override public void assertThatConfigFromSameUserPoolIsNotConflicting(JsonObject otherConfig) throws InvalidConfigException { - // TODO.. + Config.assertThatConfigFromSameUserPoolIsNotConflicting(this, otherConfig); } @Override diff --git a/src/main/java/io/supertokens/storage/postgresql/config/Config.java b/src/main/java/io/supertokens/storage/postgresql/config/Config.java index 7e2d8293..fa62cb4b 100644 --- a/src/main/java/io/supertokens/storage/postgresql/config/Config.java +++ b/src/main/java/io/supertokens/storage/postgresql/config/Config.java @@ -28,6 +28,7 @@ import io.supertokens.storage.postgresql.output.Logging; import java.io.IOException; +import java.util.HashSet; import java.util.Set; public class Config extends ResourceDistributor.SingletonResource { @@ -60,6 +61,27 @@ public static void loadConfig(Start start, JsonObject configJson, Set Logging.info(start, "Loading PostgreSQL config.", true); } + public static String getUserPoolId(Start start, JsonObject jsonConfig) throws InvalidConfigException { + // this function returns a unique string per connection pool. + // TODO: The way things are implemented right now, this function has the issue that if the user points to the + // same database, but with a different host (cause the db is reachable via two hosts as an example), + // then it will return two different user pool IDs - which is technically the wrong thing to do. + Set temp = new HashSet(); + temp.add(LOG_LEVEL.NONE); + PostgreSQLConfig config = new Config(start, jsonConfig, temp).config; + return config.getDatabaseName() + "|" + config.getHostName() + "|" + config.getTableSchema() + "|" + + config.getPort() + "|" + config.getTablePrefix(); + } + + public static void assertThatConfigFromSameUserPoolIsNotConflicting(Start start, JsonObject otherConfigJson) + throws InvalidConfigException { + Set temp = new HashSet(); + temp.add(LOG_LEVEL.NONE); + PostgreSQLConfig otherConfig = new Config(start, otherConfigJson, temp).config; + PostgreSQLConfig thisConfig = getConfig(start); + thisConfig.assertThatConfigFromSameUserPoolIsNotConflicting(otherConfig); + } + public static PostgreSQLConfig getConfig(Start start) { if (getInstance(start) == null) { throw new QuitProgramFromPluginException("Please call loadConfig() before calling getConfig()"); diff --git a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java index a6e13074..6b1bed3b 100644 --- a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java +++ b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java @@ -297,10 +297,14 @@ public String getUserIdMappingTable() { return addSchemaAndPrefixToTableName("userid_mapping"); } + public String getTablePrefix() { + return postgresql_table_names_prefix.trim(); + } + private String addSchemaAndPrefixToTableName(String tableName) { String name = tableName; - if (!postgresql_table_names_prefix.trim().equals("")) { - name = postgresql_table_names_prefix.trim() + "_" + name; + if (!getTablePrefix().equals("")) { + name = getTablePrefix() + "_" + name; } return addSchemaToTableName(name); } @@ -336,4 +340,51 @@ void validate() throws InvalidConfigException { } } + void assertThatConfigFromSameUserPoolIsNotConflicting(PostgreSQLConfig otherConfig) throws InvalidConfigException { + if (!otherConfig.postgresql_key_value_table_name.equals(postgresql_key_value_table_name)) { + throw new InvalidConfigException( + "You cannot set different name for table " + postgresql_key_value_table_name + + " for the same user pool"); + } + if (!otherConfig.postgresql_session_info_table_name.equals(postgresql_session_info_table_name)) { + throw new InvalidConfigException( + "You cannot set different name for table " + postgresql_session_info_table_name + + " for the same user pool"); + } + + if (!otherConfig.postgresql_emailpassword_users_table_name.equals(postgresql_emailpassword_users_table_name)) { + throw new InvalidConfigException( + "You cannot set different name for table " + postgresql_emailpassword_users_table_name + + " for the same user pool"); + } + + if (!otherConfig.postgresql_emailpassword_pswd_reset_tokens_table_name.equals( + postgresql_emailpassword_pswd_reset_tokens_table_name)) { + throw new InvalidConfigException( + "You cannot set different name for table " + postgresql_emailpassword_pswd_reset_tokens_table_name + + " for the same user pool"); + } + + if (!otherConfig.postgresql_emailverification_tokens_table_name.equals( + postgresql_emailverification_tokens_table_name)) { + throw new InvalidConfigException( + "You cannot set different name for table " + postgresql_emailverification_tokens_table_name + + " for the same user pool"); + } + + if (!otherConfig.postgresql_emailverification_verified_emails_table_name.equals( + postgresql_emailverification_verified_emails_table_name)) { + throw new InvalidConfigException( + "You cannot set different name for table " + + postgresql_emailverification_verified_emails_table_name + + " for the same user pool"); + } + + if (!otherConfig.postgresql_thirdparty_users_table_name.equals(postgresql_thirdparty_users_table_name)) { + throw new InvalidConfigException( + "You cannot set different name for table " + postgresql_thirdparty_users_table_name + + " for the same user pool"); + } + } + } \ No newline at end of file From f3228880efb1c7ab278b6829ad701ecd7ac274a8 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Thu, 19 Jan 2023 22:55:35 +0530 Subject: [PATCH 004/106] changes to tests to make them pass --- .../storage/postgresql/test/ConfigTest.java | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/test/java/io/supertokens/storage/postgresql/test/ConfigTest.java b/src/test/java/io/supertokens/storage/postgresql/test/ConfigTest.java index 1e83d106..42a3cb74 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/ConfigTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/ConfigTest.java @@ -58,7 +58,7 @@ public void beforeEach() { @Test public void testThatDefaultConfigLoadsCorrectly() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -74,7 +74,7 @@ public void testThatDefaultConfigLoadsCorrectly() throws Exception { @Test public void testThatCustomConfigLoadsCorrectly() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; Utils.setValueInConfig("postgresql_connection_pool_size", "5"); Utils.setValueInConfig("postgresql_key_value_table_name", "\"temp_name\""); @@ -92,14 +92,14 @@ public void testThatCustomConfigLoadsCorrectly() throws Exception { @Test public void testThatInvalidConfigThrowsRightError() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; Utils.setValueInConfig("postgresql_connection_pool_size", "-1"); TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); ProcessState.EventAndException e = process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.INIT_FAILURE); assertNotNull(e); - TestCase.assertEquals(e.exception.getMessage(), + TestCase.assertEquals(e.exception.getCause().getMessage(), "'postgresql_connection_pool_size' in the config.yaml file must be > 0"); process.kill(); @@ -109,7 +109,7 @@ public void testThatInvalidConfigThrowsRightError() throws Exception { @Test public void testThatMissingConfigFileThrowsError() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; ProcessBuilder pb = new ProcessBuilder("rm", "-r", "config.yaml"); pb.directory(new File(args[0])); @@ -121,7 +121,7 @@ public void testThatMissingConfigFileThrowsError() throws Exception { ProcessState.EventAndException e = process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.INIT_FAILURE); assertNotNull(e); TestCase.assertEquals(e.exception.getMessage(), - "java.io.FileNotFoundException: ../config.yaml (No such file or directory)"); + "../config.yaml (No such file or directory)"); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -130,7 +130,7 @@ public void testThatMissingConfigFileThrowsError() throws Exception { @Test public void testCustomLocationForConfigLoadsCorrectly() throws Exception { - String[] args = { "../", "configFile=../temp/config.yaml" }; + String[] args = {"../", "configFile=../temp/config.yaml"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); ProcessState.EventAndException e = process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.INIT_FAILURE); @@ -142,7 +142,7 @@ public void testCustomLocationForConfigLoadsCorrectly() throws Exception { // absolute path File f = new File("../temp/config.yaml"); - args = new String[] { "../", "configFile=" + f.getAbsolutePath() }; + args = new String[]{"../", "configFile=" + f.getAbsolutePath()}; process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -156,7 +156,7 @@ public void testCustomLocationForConfigLoadsCorrectly() throws Exception { @Test public void testBadPortInput() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; Utils.setValueInConfig("postgresql_port", "8989"); TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); @@ -183,7 +183,7 @@ public void testBadPortInput() throws Exception { @Test public void storageDisabledAndThenEnabled() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); process.getProcess().waitToInitStorageModule(); @@ -208,7 +208,7 @@ public void storageDisabledAndThenEnabled() throws Exception { @Test public void testBadHostInput() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; Utils.setValueInConfig("postgresql_host", "random"); @@ -225,7 +225,7 @@ public void testBadHostInput() throws Exception { @Test public void testThatChangeInTableNameIsCorrect() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; Utils.setValueInConfig("postgresql_key_value_table_name", "key_value_table"); Utils.setValueInConfig("postgresql_session_info_table_name", "session_info_table"); @@ -252,7 +252,7 @@ public void testThatChangeInTableNameIsCorrect() throws Exception { @Test public void testAddingTableNamePrefixWorks() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; Utils.setValueInConfig("postgresql_key_value_table_name", "key_value_table"); Utils.setValueInConfig("postgresql_table_names_prefix", "some_prefix"); @@ -279,7 +279,7 @@ public void testAddingTableNamePrefixWorks() throws Exception { @Test public void testAddingSchemaWorks() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; Utils.setValueInConfig("postgresql_table_schema", "myschema"); Utils.setValueInConfig("postgresql_table_names_prefix", "some_prefix"); @@ -325,7 +325,7 @@ public void testValidConnectionURI() throws Exception { PostgreSQLConfig userConfig = mapper.readValue(new File("../config.yaml"), PostgreSQLConfig.class); String hostname = userConfig.getHostName(); { - String[] args = { "../" }; + String[] args = {"../"}; Utils.setValueInConfig("postgresql_connection_uri", "postgresql://root:root@" + hostname + ":5432/supertokens"); @@ -346,7 +346,7 @@ public void testValidConnectionURI() throws Exception { { Utils.reset(); - String[] args = { "../" }; + String[] args = {"../"}; Utils.setValueInConfig("postgresql_connection_uri", "postgresql://root:root@" + hostname + "/supertokens"); Utils.commentConfigValue("postgresql_password"); @@ -366,7 +366,7 @@ public void testValidConnectionURI() throws Exception { { Utils.reset(); - String[] args = { "../" }; + String[] args = {"../"}; Utils.setValueInConfig("postgresql_connection_uri", "postgresql://" + hostname + ":5432/supertokens"); Utils.commentConfigValue("postgresql_port"); @@ -384,7 +384,7 @@ public void testValidConnectionURI() throws Exception { { Utils.reset(); - String[] args = { "../" }; + String[] args = {"../"}; Utils.setValueInConfig("postgresql_connection_uri", "postgresql://root@" + hostname + ":5432/supertokens"); Utils.commentConfigValue("postgresql_user"); @@ -403,7 +403,7 @@ public void testValidConnectionURI() throws Exception { { Utils.reset(); - String[] args = { "../" }; + String[] args = {"../"}; Utils.setValueInConfig("postgresql_connection_uri", "postgresql://root:root@" + hostname + ":5432"); Utils.commentConfigValue("postgresql_password"); @@ -428,7 +428,7 @@ public void testInvalidConnectionURI() throws Exception { PostgreSQLConfig userConfig = mapper.readValue(new File("../config.yaml"), PostgreSQLConfig.class); String hostname = userConfig.getHostName(); { - String[] args = { "../" }; + String[] args = {"../"}; Utils.setValueInConfig("postgresql_connection_uri", ":/localhost:5432/supertokens"); @@ -438,7 +438,7 @@ public void testInvalidConnectionURI() throws Exception { assertEquals( "The provided postgresql connection URI has an incorrect format. Please use a format like " + "postgresql://[user[:[password]]@]host[:port][/dbname][?attr1=val1&attr2=val2...", - e.exception.getMessage()); + e.exception.getCause().getMessage()); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -446,7 +446,7 @@ public void testInvalidConnectionURI() throws Exception { { Utils.reset(); - String[] args = { "../" }; + String[] args = {"../"}; Utils.setValueInConfig("postgresql_connection_uri", "postgresql://root:wrongPassword@" + hostname + ":5432/supertokens"); @@ -460,7 +460,7 @@ public void testInvalidConnectionURI() throws Exception { ProcessState.EventAndException e = process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.INIT_FAILURE); assertNotNull(e); - TestCase.assertTrue(e.exception.getMessage().contains("password authentication failed")); + TestCase.assertTrue(e.exception.getCause().getMessage().contains("password authentication failed")); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -473,7 +473,7 @@ public void testValidConnectionURIAttributes() throws Exception { PostgreSQLConfig userConfig = mapper.readValue(new File("../config.yaml"), PostgreSQLConfig.class); String hostname = userConfig.getHostName(); { - String[] args = { "../" }; + String[] args = {"../"}; Utils.setValueInConfig("postgresql_connection_uri", "postgresql://root:root@" + hostname + ":5432/supertokens?key1=value1"); @@ -489,7 +489,7 @@ public void testValidConnectionURIAttributes() throws Exception { { Utils.reset(); - String[] args = { "../" }; + String[] args = {"../"}; Utils.setValueInConfig("postgresql_connection_uri", "postgresql://root:root@" + hostname + ":5432/supertokens?key1=value1&allowPublicKeyRetrieval=false&key2" + "=value2"); From dacfeef0cd7a9cbc9e08ccf78612f0703689e591 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Sat, 21 Jan 2023 15:26:23 +0530 Subject: [PATCH 005/106] adds skeleton for multi tenancy functions --- .../supertokens/storage/postgresql/Start.java | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index dbf061cd..c79dc677 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -43,6 +43,10 @@ import io.supertokens.pluginInterface.jwt.JWTSigningKeyInfo; import io.supertokens.pluginInterface.jwt.exceptions.DuplicateKeyIdException; import io.supertokens.pluginInterface.jwt.sqlstorage.JWTRecipeSQLStorage; +import io.supertokens.pluginInterface.multitenancy.MultitenancyStorage; +import io.supertokens.pluginInterface.multitenancy.TenantConfig; +import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateTenantException; +import io.supertokens.pluginInterface.multitenancy.exceptions.UnknownTenantException; import io.supertokens.pluginInterface.passwordless.PasswordlessCode; import io.supertokens.pluginInterface.passwordless.PasswordlessDevice; import io.supertokens.pluginInterface.passwordless.exception.*; @@ -85,7 +89,8 @@ public class Start implements SessionSQLStorage, EmailPasswordSQLStorage, EmailVerificationSQLStorage, ThirdPartySQLStorage, - JWTRecipeSQLStorage, PasswordlessSQLStorage, UserMetadataSQLStorage, UserRolesSQLStorage, UserIdMappingStorage { + JWTRecipeSQLStorage, PasswordlessSQLStorage, UserMetadataSQLStorage, UserRolesSQLStorage, UserIdMappingStorage, + MultitenancyStorage { private static final Object appenderLock = new Object(); public static boolean silent = false; @@ -1836,4 +1841,37 @@ public HashMap getUserIdMappingForSuperTokensIds(ArrayList Date: Mon, 23 Jan 2023 12:11:20 +0530 Subject: [PATCH 006/106] fixes bug --- .../postgresql/config/PostgreSQLConfig.java | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java index 6b1bed3b..d09b4de2 100644 --- a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java +++ b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java @@ -341,48 +341,48 @@ void validate() throws InvalidConfigException { } void assertThatConfigFromSameUserPoolIsNotConflicting(PostgreSQLConfig otherConfig) throws InvalidConfigException { - if (!otherConfig.postgresql_key_value_table_name.equals(postgresql_key_value_table_name)) { + if (!otherConfig.getKeyValueTable().equals(getKeyValueTable())) { throw new InvalidConfigException( - "You cannot set different name for table " + postgresql_key_value_table_name + + "You cannot set different name for table " + getKeyValueTable() + " for the same user pool"); } - if (!otherConfig.postgresql_session_info_table_name.equals(postgresql_session_info_table_name)) { + if (!otherConfig.getSessionInfoTable().equals(getSessionInfoTable())) { throw new InvalidConfigException( - "You cannot set different name for table " + postgresql_session_info_table_name + + "You cannot set different name for table " + getSessionInfoTable() + " for the same user pool"); } - if (!otherConfig.postgresql_emailpassword_users_table_name.equals(postgresql_emailpassword_users_table_name)) { + if (!otherConfig.getEmailPasswordUsersTable().equals(getEmailPasswordUsersTable())) { throw new InvalidConfigException( - "You cannot set different name for table " + postgresql_emailpassword_users_table_name + + "You cannot set different name for table " + getEmailPasswordUsersTable() + " for the same user pool"); } - if (!otherConfig.postgresql_emailpassword_pswd_reset_tokens_table_name.equals( - postgresql_emailpassword_pswd_reset_tokens_table_name)) { + if (!otherConfig.getPasswordResetTokensTable().equals( + getPasswordResetTokensTable())) { throw new InvalidConfigException( - "You cannot set different name for table " + postgresql_emailpassword_pswd_reset_tokens_table_name + + "You cannot set different name for table " + getPasswordResetTokensTable() + " for the same user pool"); } - if (!otherConfig.postgresql_emailverification_tokens_table_name.equals( - postgresql_emailverification_tokens_table_name)) { + if (!otherConfig.getEmailVerificationTokensTable().equals( + getEmailVerificationTokensTable())) { throw new InvalidConfigException( - "You cannot set different name for table " + postgresql_emailverification_tokens_table_name + + "You cannot set different name for table " + getEmailVerificationTokensTable() + " for the same user pool"); } - if (!otherConfig.postgresql_emailverification_verified_emails_table_name.equals( - postgresql_emailverification_verified_emails_table_name)) { + if (!otherConfig.getEmailVerificationTable().equals( + getEmailVerificationTable())) { throw new InvalidConfigException( "You cannot set different name for table " + - postgresql_emailverification_verified_emails_table_name + + getEmailVerificationTable() + " for the same user pool"); } - if (!otherConfig.postgresql_thirdparty_users_table_name.equals(postgresql_thirdparty_users_table_name)) { + if (!otherConfig.getThirdPartyUsersTable().equals(getThirdPartyUsersTable())) { throw new InvalidConfigException( - "You cannot set different name for table " + postgresql_thirdparty_users_table_name + + "You cannot set different name for table " + getThirdPartyUsersTable() + " for the same user pool"); } } From 4178085e8f59bb2ba729f4e7271d1cc5f2de2351 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Mon, 23 Jan 2023 13:19:28 +0530 Subject: [PATCH 007/106] adds connection pool ID function --- .../java/io/supertokens/storage/postgresql/Start.java | 5 +++++ .../io/supertokens/storage/postgresql/config/Config.java | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index c79dc677..4ec231a8 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -134,6 +134,11 @@ public String getUserPoolId(JsonObject jsonConfig) throws InvalidConfigException return Config.getUserPoolId(this, jsonConfig); } + @Override + public String getConnectionPoolId(JsonObject jsonConfig) throws InvalidConfigException { + return Config.getConnectionPoolId(this, jsonConfig); + } + @Override public void assertThatConfigFromSameUserPoolIsNotConflicting(JsonObject otherConfig) throws InvalidConfigException { Config.assertThatConfigFromSameUserPoolIsNotConflicting(this, otherConfig); diff --git a/src/main/java/io/supertokens/storage/postgresql/config/Config.java b/src/main/java/io/supertokens/storage/postgresql/config/Config.java index fa62cb4b..9441fd3a 100644 --- a/src/main/java/io/supertokens/storage/postgresql/config/Config.java +++ b/src/main/java/io/supertokens/storage/postgresql/config/Config.java @@ -73,6 +73,15 @@ public static String getUserPoolId(Start start, JsonObject jsonConfig) throws In config.getPort() + "|" + config.getTablePrefix(); } + public static String getConnectionPoolId(Start start, JsonObject jsonConfig) throws InvalidConfigException { + Set temp = new HashSet(); + temp.add(LOG_LEVEL.NONE); + PostgreSQLConfig config = new Config(start, jsonConfig, temp).config; + return config.getConnectionScheme() + "|" + config.getConnectionAttributes() + "|" + config.getUser() + "|" + + config.getPassword() + "|" + config.getConnectionPoolSize(); + + } + public static void assertThatConfigFromSameUserPoolIsNotConflicting(Start start, JsonObject otherConfigJson) throws InvalidConfigException { Set temp = new HashSet(); From 89761fb94337bed2d5b4f4dc41e7299a074083d1 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Mon, 23 Jan 2023 14:24:35 +0530 Subject: [PATCH 008/106] changes as per interface change --- .../io/supertokens/storage/postgresql/Start.java | 8 ++++---- .../storage/postgresql/config/Config.java | 12 ++++-------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 4ec231a8..806b34d7 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -130,13 +130,13 @@ public void loadConfig(JsonObject configJson, Set logLevels) throws I } @Override - public String getUserPoolId(JsonObject jsonConfig) throws InvalidConfigException { - return Config.getUserPoolId(this, jsonConfig); + public String getUserPoolId() { + return Config.getUserPoolId(this); } @Override - public String getConnectionPoolId(JsonObject jsonConfig) throws InvalidConfigException { - return Config.getConnectionPoolId(this, jsonConfig); + public String getConnectionPoolId() { + return Config.getConnectionPoolId(this); } @Override diff --git a/src/main/java/io/supertokens/storage/postgresql/config/Config.java b/src/main/java/io/supertokens/storage/postgresql/config/Config.java index 9441fd3a..b0cc53a1 100644 --- a/src/main/java/io/supertokens/storage/postgresql/config/Config.java +++ b/src/main/java/io/supertokens/storage/postgresql/config/Config.java @@ -61,22 +61,18 @@ public static void loadConfig(Start start, JsonObject configJson, Set Logging.info(start, "Loading PostgreSQL config.", true); } - public static String getUserPoolId(Start start, JsonObject jsonConfig) throws InvalidConfigException { + public static String getUserPoolId(Start start) { // this function returns a unique string per connection pool. // TODO: The way things are implemented right now, this function has the issue that if the user points to the // same database, but with a different host (cause the db is reachable via two hosts as an example), // then it will return two different user pool IDs - which is technically the wrong thing to do. - Set temp = new HashSet(); - temp.add(LOG_LEVEL.NONE); - PostgreSQLConfig config = new Config(start, jsonConfig, temp).config; + PostgreSQLConfig config = getConfig(start); return config.getDatabaseName() + "|" + config.getHostName() + "|" + config.getTableSchema() + "|" + config.getPort() + "|" + config.getTablePrefix(); } - public static String getConnectionPoolId(Start start, JsonObject jsonConfig) throws InvalidConfigException { - Set temp = new HashSet(); - temp.add(LOG_LEVEL.NONE); - PostgreSQLConfig config = new Config(start, jsonConfig, temp).config; + public static String getConnectionPoolId(Start start) { + PostgreSQLConfig config = getConfig(start); return config.getConnectionScheme() + "|" + config.getConnectionAttributes() + "|" + config.getUser() + "|" + config.getPassword() + "|" + config.getConnectionPoolSize(); From b7b8b5d326b35c303ec1370ac8bf6377ea3170cd Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 24 Jan 2023 13:02:06 +0530 Subject: [PATCH 009/106] adds one test for multi tenany storage layer --- .../test/TestingProcessManager.java | 18 +- .../storage/postgresql/test/Utils.java | 8 +- .../test/multitenancy/StorageLayerTest.java | 304 ++++++++++++++++++ 3 files changed, 318 insertions(+), 12 deletions(-) create mode 100644 src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java diff --git a/src/test/java/io/supertokens/storage/postgresql/test/TestingProcessManager.java b/src/test/java/io/supertokens/storage/postgresql/test/TestingProcessManager.java index ec0178ea..72b080d4 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/TestingProcessManager.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/TestingProcessManager.java @@ -26,13 +26,13 @@ import java.util.ArrayList; -class TestingProcessManager { +public class TestingProcessManager { private static final ArrayList alive = new ArrayList<>(); static void deleteAllInformation() throws Exception { System.out.println("----------DELETE ALL INFORMATION----------"); - String[] args = { "../" }; + String[] args = {"../"}; TestingProcess process = TestingProcessManager.start(args); process.checkOrWaitForEvent(PROCESS_STATE.STARTED); process.main.deleteAllInformationForTesting(); @@ -121,7 +121,7 @@ public void startProcess() { } } - Main getProcess() { + public Main getProcess() { return main; } @@ -129,7 +129,7 @@ String[] getArgs() { return args; } - void kill() throws InterruptedException { + public void kill() throws InterruptedException { if (killed) { return; } @@ -137,11 +137,12 @@ void kill() throws InterruptedException { killed = true; } - EventAndException checkOrWaitForEvent(PROCESS_STATE state) throws InterruptedException { + public EventAndException checkOrWaitForEvent(PROCESS_STATE state) throws InterruptedException { return checkOrWaitForEvent(state, 15000); } - EventAndException checkOrWaitForEvent(PROCESS_STATE state, long timeToWaitMS) throws InterruptedException { + public EventAndException checkOrWaitForEvent(PROCESS_STATE state, long timeToWaitMS) + throws InterruptedException { EventAndException e = ProcessState.getInstance(main).getLastEventByName(state); if (e == null) { // we shall now wait until some time as passed. @@ -163,8 +164,9 @@ io.supertokens.storage.postgresql.ProcessState.EventAndException checkOrWaitForE io.supertokens.storage.postgresql.ProcessState.PROCESS_STATE state, long timeToWaitMS) throws InterruptedException { Start start = (Start) StorageLayer.getStorage(main); - io.supertokens.storage.postgresql.ProcessState.EventAndException e = io.supertokens.storage.postgresql.ProcessState - .getInstance(start).getLastEventByName(state); + io.supertokens.storage.postgresql.ProcessState.EventAndException e = + io.supertokens.storage.postgresql.ProcessState + .getInstance(start).getLastEventByName(state); if (e == null) { // we shall now wait until some time as passed. final long startTime = System.currentTimeMillis(); diff --git a/src/test/java/io/supertokens/storage/postgresql/test/Utils.java b/src/test/java/io/supertokens/storage/postgresql/test/Utils.java index aac4a159..161d2e8c 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/Utils.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/Utils.java @@ -35,11 +35,11 @@ import java.util.Arrays; import java.util.List; -abstract class Utils extends Mockito { +public abstract class Utils extends Mockito { private static ByteArrayOutputStream byteArrayOutputStream; - static void afterTesting() { + public static void afterTesting() { String installDir = "../"; try { // we remove the license key file @@ -73,7 +73,7 @@ static void afterTesting() { } } - static void reset() { + public static void reset() { Main.isTesting = true; PluginInterfaceTesting.isTesting = true; Start.isTesting = true; @@ -173,7 +173,7 @@ public static void commentConfigValue(String key) throws IOException { } - static TestRule getOnFailure() { + public static TestRule getOnFailure() { return new TestWatcher() { @Override protected void failed(Throwable e, Description description) { diff --git a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java new file mode 100644 index 00000000..51471959 --- /dev/null +++ b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java @@ -0,0 +1,304 @@ +/* + * 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.storage.postgresql.test.multitenancy; + +import io.supertokens.ProcessState; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.storage.postgresql.test.TestingProcessManager; +import io.supertokens.storage.postgresql.test.Utils; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import java.io.IOException; + +import static org.junit.Assert.*; + +public class StorageLayerTest { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + @Test + public void normalDbConfigErrorContinuesToWork() throws InterruptedException, IOException { + String[] args = {"../"}; + + Utils.setValueInConfig("postgresql_connection_pool_size", "-1"); + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + + ProcessState.EventAndException e = process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.INIT_FAILURE); + assertNotNull(e); + assertEquals(e.exception.getCause().getMessage(), + "'postgresql_connection_pool_size' in the config.yaml file must be > 0"); + + assertNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.LOADING_ALL_TENANT_STORAGE, 1000)); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } +// +// @Test +// public void mergingTenantWithBaseConfigWorks() throws InterruptedException, IOException, InvalidConfigException { +// String[] args = {"../"}; +// +// Utils.setValueInConfig("refresh_token_validity", "144001"); +// Utils.setValueInConfig("access_token_signing_key_dynamic", "false"); +// 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)); +// +// JsonObject tenantConfig = new JsonObject(); +// tenantConfig.add("refresh_token_validity", new JsonPrimitive(144002)); +// tenantConfig.add("password_reset_token_lifetime", new JsonPrimitive(3600001)); +// +// Config.loadAllTenantConfig(process.getProcess(), new TenantConfig[]{ +// new TenantConfig("abc", null, new EmailPasswordConfig(false), +// new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), +// new PasswordlessConfig(false), +// tenantConfig)}); +// +// Assert.assertEquals(Config.getConfig(process.getProcess()).getRefreshTokenValidity(), +// (long) 144001 * 60 * 1000); +// Assert.assertEquals(Config.getConfig(process.getProcess()).getPasswordResetTokenLifetime(), +// 3600000); +// Assert.assertEquals(Config.getConfig(process.getProcess()).getPasswordlessMaxCodeInputAttempts(), +// 5); +// Assert.assertEquals(Config.getConfig(process.getProcess()).getAccessTokenSigningKeyDynamic(), +// false); +// +// Assert.assertEquals(Config.getConfig("abc", null, process.getProcess()).getRefreshTokenValidity(), +// (long) 144002 * 60 * 1000); +// Assert.assertEquals(Config.getConfig("abc", null, process.getProcess()).getPasswordResetTokenLifetime(), +// 3600001); +// Assert.assertEquals(Config.getConfig("abc", null, process.getProcess()).getPasswordlessMaxCodeInputAttempts(), +// 5); +// Assert.assertEquals(Config.getConfig("abc", null, process.getProcess()).getAccessTokenSigningKeyDynamic(), +// false); +// +// process.kill(); +// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); +// } +// +// @Test +// public void mergingTenantWithBaseConfigWithInvalidConfigThrowsErrorWorks() +// throws InterruptedException, IOException { +// String[] args = {"../"}; +// +// Utils.setValueInConfig("refresh_token_validity", "144001"); +// Utils.setValueInConfig("access_token_signing_key_dynamic", "false"); +// TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); +// FeatureFlagTestContent.getInstance(process.getProcess()) +// .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY}); +// CoreConfigTestContent.getInstance(process.main) +// .setKeyValue(CoreConfigTestContent.VALIDITY_TESTING, true); +// process.startProcess(); +// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); +// +// JsonObject tenantConfig = new JsonObject(); +// tenantConfig.add("refresh_token_validity", new JsonPrimitive(1)); +// tenantConfig.add("password_reset_token_lifetime", new JsonPrimitive(3600001)); +// +// try { +// Config.loadAllTenantConfig(process.getProcess(), new TenantConfig[]{ +// new TenantConfig("abc", null, new EmailPasswordConfig(false), +// new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), +// new PasswordlessConfig(false), +// tenantConfig)}); +// fail(); +// } catch (InvalidConfigException e) { +// assert (e.getMessage() +// .contains("'refresh_token_validity' must be strictly greater than 'access_token_validity'")); +// } +// +// process.kill(); +// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); +// } +// +// @Test +// public void mergingTenantWithBaseConfigWithConflictingConfigsThrowsError() +// throws InterruptedException, IOException { +// String[] args = {"../"}; +// +// TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); +// FeatureFlagTestContent.getInstance(process.getProcess()) +// .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY}); +// CoreConfigTestContent.getInstance(process.main) +// .setKeyValue(CoreConfigTestContent.VALIDITY_TESTING, true); +// process.startProcess(); +// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); +// +// JsonObject tenantConfig = new JsonObject(); +// tenantConfig.add("access_token_signing_key_dynamic", new JsonPrimitive(false)); +// +// try { +// Config.loadAllTenantConfig(process.getProcess(), new TenantConfig[]{ +// new TenantConfig("abc", null, new EmailPasswordConfig(false), +// new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), +// new PasswordlessConfig(false), +// tenantConfig)}); +// fail(); +// } catch (InvalidConfigException e) { +// assert (e.getMessage() +// .equals("You cannot set different values for access_token_signing_key_dynamic for the same user +// " + +// "pool")); +// } +// +// process.kill(); +// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); +// } +// +// @Test +// public void mergingDifferentUserPoolTenantWithBaseConfigWithConflictingConfigsShouldNotThrowsError() +// throws InterruptedException, IOException, InvalidConfigException { +// String[] args = {"../"}; +// +// TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); +// FeatureFlagTestContent.getInstance(process.getProcess()) +// .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY}); +// CoreConfigTestContent.getInstance(process.main) +// .setKeyValue(CoreConfigTestContent.VALIDITY_TESTING, true); +// process.startProcess(); +// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); +// +// Storage storage = StorageLayer.getStorage(process.getProcess()); +// if (storage.getType() == STORAGE_TYPE.SQL +// && !Version.getVersion(process.getProcess()).getPluginName().equals("sqlite")) { +// JsonObject tenantConfig = new JsonObject(); +// +// if (Version.getVersion(process.getProcess()).getPluginName().equals("postgresql")) { +// tenantConfig.add("postgresql_database_name", new JsonPrimitive("random")); +// } else if (Version.getVersion(process.getProcess()).getPluginName().equals("mysql")) { +// tenantConfig.add("mysql_database_name", new JsonPrimitive("random")); +// } else { +// tenantConfig.add("mongodb_connection_uri", new JsonPrimitive("mongodb://root:root@localhost:27018")); +// } +// tenantConfig.add("access_token_signing_key_dynamic", new JsonPrimitive(false)); +// +// Config.loadAllTenantConfig(process.getProcess(), new TenantConfig[]{ +// new TenantConfig("abc", null, new EmailPasswordConfig(false), +// new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), +// new PasswordlessConfig(false), +// tenantConfig)}); +// +// } +// +// Assert.assertEquals(Config.getConfig(process.getProcess()).getAccessTokenSigningKeyDynamic(), +// true); +// +// Assert.assertEquals(Config.getConfig("abc", null, process.getProcess()).getPasswordlessMaxCodeInputAttempts(), +// 5); +// Assert.assertEquals(Config.getConfig("abc", null, process.getProcess()).getAccessTokenSigningKeyDynamic(), +// false); +// +// process.kill(); +// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); +// } +// +// @Test +// public void testDifferentWaysToGetConfigBasedOnConnectionURIAndTenantId() +// throws InterruptedException, IOException, InvalidConfigException { +// String[] args = {"../"}; +// +// Utils.setValueInConfig("refresh_token_validity", "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)); +// +// TenantConfig[] tenants = new TenantConfig[4]; +// +// { +// JsonObject tenantConfig = new JsonObject(); +// tenantConfig.add("refresh_token_validity", new JsonPrimitive(144002)); +// tenants[0] = new TenantConfig("c1", null, new EmailPasswordConfig(false), +// new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), +// new PasswordlessConfig(false), +// tenantConfig); +// } +// +// { +// JsonObject tenantConfig = new JsonObject(); +// tenantConfig.add("refresh_token_validity", new JsonPrimitive(144003)); +// tenants[1] = new TenantConfig("c1", "t1", new EmailPasswordConfig(false), +// new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), +// new PasswordlessConfig(false), +// tenantConfig); +// } +// +// { +// JsonObject tenantConfig = new JsonObject(); +// tenantConfig.add("refresh_token_validity", new JsonPrimitive(144004)); +// tenants[2] = new TenantConfig(null, "t2", new EmailPasswordConfig(false), +// new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), +// new PasswordlessConfig(false), +// tenantConfig); +// } +// +// { +// JsonObject tenantConfig = new JsonObject(); +// tenantConfig.add("refresh_token_validity", new JsonPrimitive(144005)); +// tenants[3] = new TenantConfig(null, "t1", new EmailPasswordConfig(false), +// new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), +// new PasswordlessConfig(false), +// tenantConfig); +// } +// +// Config.loadAllTenantConfig(process.getProcess(), tenants); +// +// Assert.assertEquals(Config.getConfig(null, null, process.getProcess()).getRefreshTokenValidity(), +// (long) 144001 * 60 * 1000); +// Assert.assertEquals(Config.getConfig("c1", null, process.getProcess()).getRefreshTokenValidity(), +// (long) 144002 * 60 * 1000); +// Assert.assertEquals(Config.getConfig("c1", "t1", process.getProcess()).getRefreshTokenValidity(), +// (long) 144003 * 60 * 1000); +// Assert.assertEquals(Config.getConfig(null, "t1", process.getProcess()).getRefreshTokenValidity(), +// (long) 144005 * 60 * 1000); +// Assert.assertEquals(Config.getConfig("c2", null, process.getProcess()).getRefreshTokenValidity(), +// (long) 144001 * 60 * 1000); +// Assert.assertEquals(Config.getConfig("c2", "t1", process.getProcess()).getRefreshTokenValidity(), +// (long) 144005 * 60 * 1000); +// Assert.assertEquals(Config.getConfig("c3", "t2", process.getProcess()).getRefreshTokenValidity(), +// (long) 144004 * 60 * 1000); +// Assert.assertEquals(Config.getConfig("c1", "t2", process.getProcess()).getRefreshTokenValidity(), +// (long) 144002 * 60 * 1000); +// Assert.assertEquals(Config.getConfig(null, "t2", process.getProcess()).getRefreshTokenValidity(), +// (long) 144004 * 60 * 1000); +// +// +// process.kill(); +// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); +// } +} From 787d49c9da4a296c2625a5b1fc045928a00e9a27 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 24 Jan 2023 14:03:41 +0530 Subject: [PATCH 010/106] adds more tests --- .../test/multitenancy/StorageLayerTest.java | 99 ++++++++++--------- 1 file changed, 51 insertions(+), 48 deletions(-) diff --git a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java index 51471959..96494d42 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java @@ -16,15 +16,22 @@ package io.supertokens.storage.postgresql.test.multitenancy; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; import io.supertokens.ProcessState; +import io.supertokens.config.Config; import io.supertokens.featureflag.EE_FEATURES; import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.pluginInterface.exceptions.InvalidConfigException; +import io.supertokens.pluginInterface.multitenancy.EmailPasswordConfig; +import io.supertokens.pluginInterface.multitenancy.PasswordlessConfig; +import io.supertokens.pluginInterface.multitenancy.TenantConfig; +import io.supertokens.pluginInterface.multitenancy.ThirdPartyConfig; +import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.test.TestingProcessManager; import io.supertokens.storage.postgresql.test.Utils; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; +import io.supertokens.storageLayer.StorageLayer; +import org.junit.*; import org.junit.rules.TestRule; import java.io.IOException; @@ -65,50 +72,46 @@ public void normalDbConfigErrorContinuesToWork() throws InterruptedException, IO process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } -// -// @Test -// public void mergingTenantWithBaseConfigWorks() throws InterruptedException, IOException, InvalidConfigException { -// String[] args = {"../"}; -// -// Utils.setValueInConfig("refresh_token_validity", "144001"); -// Utils.setValueInConfig("access_token_signing_key_dynamic", "false"); -// 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)); -// -// JsonObject tenantConfig = new JsonObject(); -// tenantConfig.add("refresh_token_validity", new JsonPrimitive(144002)); -// tenantConfig.add("password_reset_token_lifetime", new JsonPrimitive(3600001)); -// -// Config.loadAllTenantConfig(process.getProcess(), new TenantConfig[]{ -// new TenantConfig("abc", null, new EmailPasswordConfig(false), -// new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), -// new PasswordlessConfig(false), -// tenantConfig)}); -// -// Assert.assertEquals(Config.getConfig(process.getProcess()).getRefreshTokenValidity(), -// (long) 144001 * 60 * 1000); -// Assert.assertEquals(Config.getConfig(process.getProcess()).getPasswordResetTokenLifetime(), -// 3600000); -// Assert.assertEquals(Config.getConfig(process.getProcess()).getPasswordlessMaxCodeInputAttempts(), -// 5); -// Assert.assertEquals(Config.getConfig(process.getProcess()).getAccessTokenSigningKeyDynamic(), -// false); -// -// Assert.assertEquals(Config.getConfig("abc", null, process.getProcess()).getRefreshTokenValidity(), -// (long) 144002 * 60 * 1000); -// Assert.assertEquals(Config.getConfig("abc", null, process.getProcess()).getPasswordResetTokenLifetime(), -// 3600001); -// Assert.assertEquals(Config.getConfig("abc", null, process.getProcess()).getPasswordlessMaxCodeInputAttempts(), -// 5); -// Assert.assertEquals(Config.getConfig("abc", null, process.getProcess()).getAccessTokenSigningKeyDynamic(), -// false); -// -// process.kill(); -// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); -// } + + @Test + public void mergingTenantWithBaseConfigWorks() + throws InterruptedException, IOException, InvalidConfigException { + 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)); + + JsonObject tenantConfig = new JsonObject(); + tenantConfig.add("postgresql_table_names_prefix", new JsonPrimitive("test")); + + TenantConfig[] tenants = new TenantConfig[]{ + new TenantConfig("abc", null, new EmailPasswordConfig(false), + new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), + new PasswordlessConfig(false), + tenantConfig)}; + + Config.loadAllTenantConfig(process.getProcess(), tenants); + + StorageLayer.loadAllTenantStorage(process.getProcess(), tenants); + + Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( + (Start) StorageLayer.getStorage(null, null, process.getProcess())) + .getTablePrefix(), ""); + + Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( + (Start) StorageLayer.getStorage("abc", null, process.getProcess())) + .getTablePrefix(), "test"); + + Assert.assertEquals( + process.getProcess().getResourceDistributor().getAllResourcesWithResourceKey(StorageLayer.RESOURCE_KEY) + .size(), 2); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } // // @Test // public void mergingTenantWithBaseConfigWithInvalidConfigThrowsErrorWorks() From 32680eeea102dc53c808f3a7cd33aa8ec1aad6c2 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 24 Jan 2023 19:09:00 +0530 Subject: [PATCH 011/106] fixes bugs --- .../storage/postgresql/ConnectionPool.java | 43 +- .../supertokens/storage/postgresql/Start.java | 13 +- .../storage/postgresql/config/Config.java | 2 +- .../postgresql/config/PostgreSQLConfig.java | 5 + .../storage/postgresql/test/ConfigTest.java | 5 +- .../postgresql/test/InMemoryDBTest.java | 20 +- .../test/multitenancy/StorageLayerTest.java | 447 +++++++++++++----- 7 files changed, 376 insertions(+), 159 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/ConnectionPool.java b/src/main/java/io/supertokens/storage/postgresql/ConnectionPool.java index 7041620c..654f0cab 100644 --- a/src/main/java/io/supertokens/storage/postgresql/ConnectionPool.java +++ b/src/main/java/io/supertokens/storage/postgresql/ConnectionPool.java @@ -19,6 +19,7 @@ import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; +import io.supertokens.pluginInterface.exceptions.DbInitException; import io.supertokens.pluginInterface.exceptions.QuitProgramFromPluginException; import io.supertokens.storage.postgresql.config.Config; import io.supertokens.storage.postgresql.config.PostgreSQLConfig; @@ -33,24 +34,13 @@ public class ConnectionPool extends ResourceDistributor.SingletonResource { private static final String RESOURCE_KEY = "io.supertokens.storage.postgresql.ConnectionPool"; - private static HikariDataSource hikariDataSource = null; + private final HikariDataSource hikariDataSource; private ConnectionPool(Start start) { if (!start.enabled) { throw new RuntimeException("Connection to refused"); // emulates exception thrown by Hikari } - if (ConnectionPool.hikariDataSource != null) { - // This implies that it was already created before and that - // there is no need to create Hikari again. - - // If ConnectionPool.hikariDataSource == null, it implies that - // either the config file had changed somehow (which means the plugin JAR was reloaded, resulting in static - // variables to be set to null), or it means that this is the first time we are trying to connect to a db - // (applicable only for testing). - return; - } - HikariConfig config = new HikariConfig(); PostgreSQLConfig userConfig = Config.getConfig(start); config.setDriverClassName("org.postgresql.Driver"); @@ -92,7 +82,7 @@ private ConnectionPool(Start start) { // SuperTokens // - Failed to validate connection org.mariadb.jdbc.MariaDbConnection@79af83ae (Connection.setNetworkTimeout // cannot be called on a closed connection). Possibly consider using a shorter maxLifetime value. - config.setPoolName("SuperTokens"); + config.setPoolName(start.getUserPoolId() + "~" + start.getConnectionPoolId()); hikariDataSource = new HikariDataSource(config); } @@ -120,19 +110,25 @@ private static ConnectionPool getInstance(Start start) { return (ConnectionPool) start.getResourceDistributor().getResource(RESOURCE_KEY); } - static void initPool(Start start) { - if (getInstance(start) != null) { + static boolean isAlreadyInitialised(Start start) { + return getInstance(start) != null; + } + + static void initPool(Start start) throws DbInitException { + if (isAlreadyInitialised(start)) { return; } if (Thread.currentThread() != start.mainThread) { - throw new QuitProgramFromPluginException("Should not come here"); + throw new DbInitException("Should not come here"); } Logging.info(start, "Setting up PostgreSQL connection pool.", true); boolean longMessagePrinted = false; long maxTryTime = System.currentTimeMillis() + getTimeToWaitToInit(start); - String errorMessage = "Error connecting to PostgreSQL instance. Please make sure that PostgreSQL is running and that " - + "you have" + " specified the correct values for ('postgresql_host' and 'postgresql_port') or for " - + "'postgresql_connection_uri'"; + String errorMessage = + "Error connecting to PostgreSQL instance. Please make sure that PostgreSQL is running and that " + + "you have" + + " specified the correct values for ('postgresql_host' and 'postgresql_port') or for " + + "'postgresql_connection_uri'"; try { while (true) { try { @@ -143,7 +139,7 @@ static void initPool(Start start) { || e.getMessage().contains("the database system is starting up")) { start.handleKillSignalForWhenItHappens(); if (System.currentTimeMillis() > maxTryTime) { - throw new QuitProgramFromPluginException(errorMessage); + throw new DbInitException(errorMessage); } if (!longMessagePrinted) { longMessagePrinted = true; @@ -160,7 +156,7 @@ static void initPool(Start start) { } Thread.sleep(getRetryIntervalIfInitFails(start)); } catch (InterruptedException ex) { - throw new QuitProgramFromPluginException(errorMessage); + throw new DbInitException(errorMessage); } } else { throw e; @@ -179,14 +175,13 @@ public static Connection getConnection(Start start) throws SQLException { if (!start.enabled) { throw new SQLException("Storage layer disabled"); } - return ConnectionPool.hikariDataSource.getConnection(); + return getInstance(start).hikariDataSource.getConnection(); } static void close(Start start) { if (getInstance(start) == null) { return; } - ConnectionPool.hikariDataSource.close(); - ConnectionPool.hikariDataSource = null; + getInstance(start).hikariDataSource.close(); } } diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 806b34d7..7ee9a3cd 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -35,8 +35,8 @@ import io.supertokens.pluginInterface.emailverification.EmailVerificationTokenInfo; import io.supertokens.pluginInterface.emailverification.exception.DuplicateEmailVerificationTokenException; import io.supertokens.pluginInterface.emailverification.sqlStorage.EmailVerificationSQLStorage; +import io.supertokens.pluginInterface.exceptions.DbInitException; import io.supertokens.pluginInterface.exceptions.InvalidConfigException; -import io.supertokens.pluginInterface.exceptions.QuitProgramFromPluginException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.jwt.JWTRecipeStorage; @@ -183,12 +183,15 @@ public void stopLogging() { } @Override - public void initStorage() { - ConnectionPool.initPool(this); + public void initStorage() throws DbInitException { + if (ConnectionPool.isAlreadyInitialised(this)) { + return; + } try { + ConnectionPool.initPool(this); GeneralQueries.createTablesIfNotExists(this); - } catch (SQLException | StorageQueryException e) { - throw new QuitProgramFromPluginException(e); + } catch (Exception e) { + throw new DbInitException(e); } } diff --git a/src/main/java/io/supertokens/storage/postgresql/config/Config.java b/src/main/java/io/supertokens/storage/postgresql/config/Config.java index b0cc53a1..87cf5f0c 100644 --- a/src/main/java/io/supertokens/storage/postgresql/config/Config.java +++ b/src/main/java/io/supertokens/storage/postgresql/config/Config.java @@ -68,7 +68,7 @@ public static String getUserPoolId(Start start) { // then it will return two different user pool IDs - which is technically the wrong thing to do. PostgreSQLConfig config = getConfig(start); return config.getDatabaseName() + "|" + config.getHostName() + "|" + config.getTableSchema() + "|" + - config.getPort() + "|" + config.getTablePrefix(); + config.getPort(); } public static String getConnectionPoolId(Start start) { diff --git a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java index d09b4de2..2dda5a1a 100644 --- a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java +++ b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java @@ -341,6 +341,11 @@ void validate() throws InvalidConfigException { } void assertThatConfigFromSameUserPoolIsNotConflicting(PostgreSQLConfig otherConfig) throws InvalidConfigException { + if (!otherConfig.getTablePrefix().equals(getTablePrefix())) { + throw new InvalidConfigException( + "You cannot set different name for table prefix for the same user pool"); + } + if (!otherConfig.getKeyValueTable().equals(getKeyValueTable())) { throw new InvalidConfigException( "You cannot set different name for table " + getKeyValueTable() + diff --git a/src/test/java/io/supertokens/storage/postgresql/test/ConfigTest.java b/src/test/java/io/supertokens/storage/postgresql/test/ConfigTest.java index 42a3cb74..4e850f2b 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/ConfigTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/ConfigTest.java @@ -172,7 +172,7 @@ public void testBadPortInput() throws Exception { ProcessState.EventAndException e = process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.INIT_FAILURE, 7000); assertNotNull(e); - assertEquals(e.exception.getMessage(), + assertEquals(e.exception.getCause().getCause().getMessage(), "Error connecting to PostgreSQL instance. Please make sure that PostgreSQL is running and that you " + "have specified the correct values for ('postgresql_host' and 'postgresql_port') or for " + "'postgresql_connection_uri'"); @@ -216,7 +216,8 @@ public void testBadHostInput() throws Exception { ProcessState.EventAndException e = process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.INIT_FAILURE); assertNotNull(e); - assertEquals("Failed to initialize pool: The connection attempt failed.", e.exception.getMessage()); + assertEquals("Failed to initialize pool: The connection attempt failed.", + e.exception.getCause().getCause().getMessage()); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); diff --git a/src/test/java/io/supertokens/storage/postgresql/test/InMemoryDBTest.java b/src/test/java/io/supertokens/storage/postgresql/test/InMemoryDBTest.java index cb21dcb1..64773b62 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/InMemoryDBTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/InMemoryDBTest.java @@ -68,7 +68,7 @@ public void checkThatInMemDVWorksEvenIfWrongConfig() Utils.commentConfigValue("postgresql_user"); Utils.commentConfigValue("postgresql_password"); - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -91,7 +91,7 @@ public void checkThatInMemDVWorksEvenIfWrongConfig() } { - String[] args = { "../" }; + String[] args = {"../"}; StorageLayer.close(); TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -109,7 +109,7 @@ public void checkThatActualDBWorksIfCorrectConfigDev() throws InterruptedExcepti NoSuchPaddingException, BadPaddingException, UnsupportedEncodingException, InvalidKeySpecException, IllegalBlockSizeException, StorageTransactionLogicException { { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -131,7 +131,7 @@ public void checkThatActualDBWorksIfCorrectConfigDev() throws InterruptedExcepti assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -148,7 +148,7 @@ public void checkThatActualDBWorksIfCorrectConfigProduction() throws Interrupted NoSuchPaddingException, BadPaddingException, UnsupportedEncodingException, InvalidKeySpecException, IllegalBlockSizeException, StorageTransactionLogicException { { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -170,7 +170,7 @@ public void checkThatActualDBWorksIfCorrectConfigProduction() throws Interrupted assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -183,7 +183,7 @@ public void checkThatActualDBWorksIfCorrectConfigProduction() throws Interrupted @Test public void checkThatErrorIsThrownIfIncorrectConfigInProduction() throws IOException, InterruptedException { - String[] args = { "../" }; + String[] args = {"../"}; Utils.commentConfigValue("postgresql_user"); @@ -191,7 +191,7 @@ public void checkThatErrorIsThrownIfIncorrectConfigInProduction() throws IOExcep ProcessState.EventAndException e = process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.INIT_FAILURE, 15000); assertNotNull(e); - TestCase.assertEquals(e.exception.getMessage(), + TestCase.assertEquals(e.exception.getCause().getMessage(), "'postgresql_user' and 'postgresql_connection_uri' are not set. Please set at least one of " + "these values"); @@ -201,7 +201,7 @@ public void checkThatErrorIsThrownIfIncorrectConfigInProduction() throws IOExcep @Test public void ifForceNoInMemoryThenDevShouldThrowError() throws IOException, InterruptedException { - String[] args = { "../", "forceNoInMemDB=true" }; + String[] args = {"../", "forceNoInMemDB=true"}; Utils.commentConfigValue("postgresql_user"); @@ -209,7 +209,7 @@ public void ifForceNoInMemoryThenDevShouldThrowError() throws IOException, Inter ProcessState.EventAndException e = process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.INIT_FAILURE, 15000); assertNotNull(e); - TestCase.assertEquals(e.exception.getMessage(), + TestCase.assertEquals(e.exception.getCause().getMessage(), "'postgresql_user' and 'postgresql_connection_uri' are not set. Please set at least one of " + "these values"); diff --git a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java index 96494d42..89b0aa44 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java @@ -22,6 +22,8 @@ import io.supertokens.config.Config; import io.supertokens.featureflag.EE_FEATURES; import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.exceptions.DbInitException; import io.supertokens.pluginInterface.exceptions.InvalidConfigException; import io.supertokens.pluginInterface.multitenancy.EmailPasswordConfig; import io.supertokens.pluginInterface.multitenancy.PasswordlessConfig; @@ -75,7 +77,7 @@ public void normalDbConfigErrorContinuesToWork() throws InterruptedException, IO @Test public void mergingTenantWithBaseConfigWorks() - throws InterruptedException, IOException, InvalidConfigException { + throws InterruptedException, IOException, InvalidConfigException, DbInitException { String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); @@ -86,6 +88,7 @@ public void mergingTenantWithBaseConfigWorks() JsonObject tenantConfig = new JsonObject(); tenantConfig.add("postgresql_table_names_prefix", new JsonPrimitive("test")); + tenantConfig.add("postgresql_table_schema", new JsonPrimitive("random")); TenantConfig[] tenants = new TenantConfig[]{ new TenantConfig("abc", null, new EmailPasswordConfig(false), @@ -97,13 +100,339 @@ public void mergingTenantWithBaseConfigWorks() StorageLayer.loadAllTenantStorage(process.getProcess(), tenants); + assertNotSame(StorageLayer.getStorage("abc", null, process.getProcess()), + StorageLayer.getStorage(null, null, process.getProcess())); + Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( (Start) StorageLayer.getStorage(null, null, process.getProcess())) .getTablePrefix(), ""); + Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( + (Start) StorageLayer.getStorage(null, null, process.getProcess())) + .getTableSchema(), "public"); Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( (Start) StorageLayer.getStorage("abc", null, process.getProcess())) .getTablePrefix(), "test"); + Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( + (Start) StorageLayer.getStorage("abc", null, process.getProcess())) + .getTableSchema(), "random"); + + Assert.assertEquals( + process.getProcess().getResourceDistributor().getAllResourcesWithResourceKey(StorageLayer.RESOURCE_KEY) + .size(), 2); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void creatingTenantWithNoExistingDbThrowsError() + throws InterruptedException, IOException, InvalidConfigException, DbInitException { + 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)); + + JsonObject tenantConfig = new JsonObject(); + tenantConfig.add("postgresql_table_names_prefix", new JsonPrimitive("test")); + tenantConfig.add("postgresql_database_name", new JsonPrimitive("random")); + + try { + TenantConfig[] tenants = new TenantConfig[]{ + new TenantConfig("abc", null, new EmailPasswordConfig(false), + new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), + new PasswordlessConfig(false), + tenantConfig)}; + + Config.loadAllTenantConfig(process.getProcess(), tenants); + + StorageLayer.loadAllTenantStorage(process.getProcess(), tenants); + fail(); + } catch (DbInitException e) { + assertEquals(e.getMessage(), "com.zaxxer.hikari.pool.HikariPool$PoolInitializationException: Failed to " + + "initialize pool: FATAL: database \"random\" does not exist"); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void storageInstanceIsReusedAcrossTenants() + throws InterruptedException, IOException, InvalidConfigException, DbInitException { + 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)); + + JsonObject tenantConfig = new JsonObject(); + tenantConfig.add("access_token_validity", new JsonPrimitive(3601)); + + TenantConfig[] tenants = new TenantConfig[]{ + new TenantConfig("abc", null, new EmailPasswordConfig(false), + new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), + new PasswordlessConfig(false), + tenantConfig)}; + + Config.loadAllTenantConfig(process.getProcess(), tenants); + + StorageLayer.loadAllTenantStorage(process.getProcess(), tenants); + + assertSame(StorageLayer.getStorage("abc", null, process.getProcess()), + StorageLayer.getStorage(null, null, process.getProcess())); + + Assert.assertEquals(Config.getConfig(null, null, process.getProcess()).getAccessTokenValidity(), + (long) 3600 * 1000); + + Assert.assertEquals(Config.getConfig("abc", null, process.getProcess()).getAccessTokenValidity(), + (long) 3601 * 1000); + + Assert.assertEquals( + process.getProcess().getResourceDistributor().getAllResourcesWithResourceKey(StorageLayer.RESOURCE_KEY) + .size(), 2); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void storageInstanceIsReusedAcrossTenantsComplex() + throws InterruptedException, IOException, InvalidConfigException, DbInitException { + 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)); + + JsonObject tenantConfig = new JsonObject(); + tenantConfig.add("access_token_validity", new JsonPrimitive(3601)); + + JsonObject tenantConfig1 = new JsonObject(); + tenantConfig1.add("postgresql_connection_pool_size", new JsonPrimitive(11)); + + TenantConfig[] tenants = new TenantConfig[]{ + new TenantConfig("abc", null, new EmailPasswordConfig(false), + new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), + new PasswordlessConfig(false), + tenantConfig), + new TenantConfig("abc", "t1", new EmailPasswordConfig(false), + new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), + new PasswordlessConfig(false), + tenantConfig1), + new TenantConfig(null, "t2", new EmailPasswordConfig(false), + new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), + new PasswordlessConfig(false), + tenantConfig1)}; + + Config.loadAllTenantConfig(process.getProcess(), tenants); + + StorageLayer.loadAllTenantStorage(process.getProcess(), tenants); + + assertSame(StorageLayer.getStorage("abc", null, process.getProcess()), + StorageLayer.getStorage(null, null, process.getProcess())); + + assertSame(StorageLayer.getStorage("abc", "t1", process.getProcess()), + StorageLayer.getStorage(null, "t2", process.getProcess())); + + assertNotSame(StorageLayer.getStorage("abc", "t1", process.getProcess()), + StorageLayer.getStorage(null, null, process.getProcess())); + + Assert.assertEquals(Config.getConfig(null, null, process.getProcess()).getAccessTokenValidity(), + (long) 3600 * 1000); + + Assert.assertEquals(Config.getConfig("abc", null, process.getProcess()).getAccessTokenValidity(), + (long) 3601 * 1000); + + Assert.assertEquals( + process.getProcess().getResourceDistributor().getAllResourcesWithResourceKey(StorageLayer.RESOURCE_KEY) + .size(), 4); + + Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( + (Start) StorageLayer.getStorage("abc", "t1", process.getProcess())) + .getConnectionPoolSize(), 11); + + Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( + (Start) StorageLayer.getStorage("random", "t2", process.getProcess())) + .getConnectionPoolSize(), 11); + + Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( + (Start) StorageLayer.getStorage("random", null, process.getProcess())) + .getConnectionPoolSize(), 10); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void mergingTenantWithBaseConfigWithInvalidConfigThrowsErrorWorks() + throws InterruptedException, IOException, DbInitException { + 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)); + + JsonObject tenantConfig = new JsonObject(); + tenantConfig.add("postgresql_connection_pool_size", new JsonPrimitive(-1)); + + try { + TenantConfig[] tenants = new TenantConfig[]{ + new TenantConfig("abc", null, new EmailPasswordConfig(false), + new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), + new PasswordlessConfig(false), + tenantConfig)}; + Config.loadAllTenantConfig(process.getProcess(), tenants); + + StorageLayer.loadAllTenantStorage(process.getProcess(), tenants); + fail(); + } catch (InvalidConfigException e) { + assert (e.getMessage() + .contains("'postgresql_connection_pool_size' in the config.yaml file must be > 0")); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void mergingTenantWithBaseConfigWithConflictingConfigsThrowsError() + throws InterruptedException, IOException, DbInitException { + 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)); + + JsonObject tenantConfig = new JsonObject(); + tenantConfig.add("postgresql_thirdparty_users_table_name", new JsonPrimitive("random")); + + try { + TenantConfig[] tenants = new TenantConfig[]{ + new TenantConfig("abc", null, new EmailPasswordConfig(false), + new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), + new PasswordlessConfig(false), + tenantConfig)}; + Config.loadAllTenantConfig(process.getProcess(), tenants); + + StorageLayer.loadAllTenantStorage(process.getProcess(), tenants); + fail(); + } catch (InvalidConfigException e) { + assertEquals(e.getMessage(), + "You cannot set different name for table random for the same user pool"); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void mergingDifferentConnectionPoolIdTenantWithBaseConfigWithConflictingConfigsShouldThrowsError() + throws InterruptedException, IOException, DbInitException { + 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)); + + JsonObject tenantConfig = new JsonObject(); + tenantConfig.add("postgresql_table_names_prefix", new JsonPrimitive("random")); + tenantConfig.add("postgresql_connection_pool_size", new JsonPrimitive(11)); + + try { + TenantConfig[] tenants = new TenantConfig[]{ + new TenantConfig("abc", null, new EmailPasswordConfig(false), + new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), + new PasswordlessConfig(false), + tenantConfig)}; + Config.loadAllTenantConfig(process.getProcess(), tenants); + + StorageLayer.loadAllTenantStorage(process.getProcess(), tenants); + fail(); + } catch (InvalidConfigException e) { + assertEquals(e.getMessage(), + "You cannot set different name for table prefix for the same user pool"); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void mergingDifferentUserPoolIdTenantWithBaseConfigWithConflictingConfigsShouldNotThrowsError() + throws InterruptedException, IOException, InvalidConfigException, DbInitException { + 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)); + + JsonObject tenantConfig = new JsonObject(); + tenantConfig.add("postgresql_thirdparty_users_table_name", new JsonPrimitive("random")); + tenantConfig.add("postgresql_connection_pool_size", new JsonPrimitive(11)); + tenantConfig.add("postgresql_table_schema", new JsonPrimitive("supertokens2")); + + TenantConfig[] tenants = new TenantConfig[]{ + new TenantConfig("abc", null, new EmailPasswordConfig(false), + new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), + new PasswordlessConfig(false), + tenantConfig)}; + Config.loadAllTenantConfig(process.getProcess(), tenants); + + StorageLayer.loadAllTenantStorage(process.getProcess(), tenants); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void newStorageIsNotCreatedWhenSameTenantIsAdded() + throws InterruptedException, IOException, InvalidConfigException, DbInitException { + 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)); + + Storage existingStorage = StorageLayer.getStorage(null, null, process.getProcess()); + + JsonObject tenantConfig = new JsonObject(); + tenantConfig.add("access_token_validity", new JsonPrimitive(3601)); + + TenantConfig[] tenants = new TenantConfig[]{ + new TenantConfig("abc", null, new EmailPasswordConfig(false), + new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), + new PasswordlessConfig(false), + tenantConfig)}; + + Config.loadAllTenantConfig(process.getProcess(), tenants); + + StorageLayer.loadAllTenantStorage(process.getProcess(), tenants); + + assertSame(StorageLayer.getStorage("abc", null, process.getProcess()), existingStorage); + + Assert.assertEquals(Config.getConfig(null, null, process.getProcess()).getAccessTokenValidity(), + (long) 3600 * 1000); + + Assert.assertEquals(Config.getConfig("abc", null, process.getProcess()).getAccessTokenValidity(), + (long) 3601 * 1000); Assert.assertEquals( process.getProcess().getResourceDistributor().getAllResourcesWithResourceKey(StorageLayer.RESOURCE_KEY) @@ -112,122 +441,6 @@ public void mergingTenantWithBaseConfigWorks() process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } -// -// @Test -// public void mergingTenantWithBaseConfigWithInvalidConfigThrowsErrorWorks() -// throws InterruptedException, IOException { -// String[] args = {"../"}; -// -// Utils.setValueInConfig("refresh_token_validity", "144001"); -// Utils.setValueInConfig("access_token_signing_key_dynamic", "false"); -// TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); -// FeatureFlagTestContent.getInstance(process.getProcess()) -// .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY}); -// CoreConfigTestContent.getInstance(process.main) -// .setKeyValue(CoreConfigTestContent.VALIDITY_TESTING, true); -// process.startProcess(); -// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); -// -// JsonObject tenantConfig = new JsonObject(); -// tenantConfig.add("refresh_token_validity", new JsonPrimitive(1)); -// tenantConfig.add("password_reset_token_lifetime", new JsonPrimitive(3600001)); -// -// try { -// Config.loadAllTenantConfig(process.getProcess(), new TenantConfig[]{ -// new TenantConfig("abc", null, new EmailPasswordConfig(false), -// new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), -// new PasswordlessConfig(false), -// tenantConfig)}); -// fail(); -// } catch (InvalidConfigException e) { -// assert (e.getMessage() -// .contains("'refresh_token_validity' must be strictly greater than 'access_token_validity'")); -// } -// -// process.kill(); -// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); -// } -// -// @Test -// public void mergingTenantWithBaseConfigWithConflictingConfigsThrowsError() -// throws InterruptedException, IOException { -// String[] args = {"../"}; -// -// TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); -// FeatureFlagTestContent.getInstance(process.getProcess()) -// .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY}); -// CoreConfigTestContent.getInstance(process.main) -// .setKeyValue(CoreConfigTestContent.VALIDITY_TESTING, true); -// process.startProcess(); -// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); -// -// JsonObject tenantConfig = new JsonObject(); -// tenantConfig.add("access_token_signing_key_dynamic", new JsonPrimitive(false)); -// -// try { -// Config.loadAllTenantConfig(process.getProcess(), new TenantConfig[]{ -// new TenantConfig("abc", null, new EmailPasswordConfig(false), -// new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), -// new PasswordlessConfig(false), -// tenantConfig)}); -// fail(); -// } catch (InvalidConfigException e) { -// assert (e.getMessage() -// .equals("You cannot set different values for access_token_signing_key_dynamic for the same user -// " + -// "pool")); -// } -// -// process.kill(); -// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); -// } -// -// @Test -// public void mergingDifferentUserPoolTenantWithBaseConfigWithConflictingConfigsShouldNotThrowsError() -// throws InterruptedException, IOException, InvalidConfigException { -// String[] args = {"../"}; -// -// TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); -// FeatureFlagTestContent.getInstance(process.getProcess()) -// .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY}); -// CoreConfigTestContent.getInstance(process.main) -// .setKeyValue(CoreConfigTestContent.VALIDITY_TESTING, true); -// process.startProcess(); -// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); -// -// Storage storage = StorageLayer.getStorage(process.getProcess()); -// if (storage.getType() == STORAGE_TYPE.SQL -// && !Version.getVersion(process.getProcess()).getPluginName().equals("sqlite")) { -// JsonObject tenantConfig = new JsonObject(); -// -// if (Version.getVersion(process.getProcess()).getPluginName().equals("postgresql")) { -// tenantConfig.add("postgresql_database_name", new JsonPrimitive("random")); -// } else if (Version.getVersion(process.getProcess()).getPluginName().equals("mysql")) { -// tenantConfig.add("mysql_database_name", new JsonPrimitive("random")); -// } else { -// tenantConfig.add("mongodb_connection_uri", new JsonPrimitive("mongodb://root:root@localhost:27018")); -// } -// tenantConfig.add("access_token_signing_key_dynamic", new JsonPrimitive(false)); -// -// Config.loadAllTenantConfig(process.getProcess(), new TenantConfig[]{ -// new TenantConfig("abc", null, new EmailPasswordConfig(false), -// new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), -// new PasswordlessConfig(false), -// tenantConfig)}); -// -// } -// -// Assert.assertEquals(Config.getConfig(process.getProcess()).getAccessTokenSigningKeyDynamic(), -// true); -// -// Assert.assertEquals(Config.getConfig("abc", null, process.getProcess()).getPasswordlessMaxCodeInputAttempts(), -// 5); -// Assert.assertEquals(Config.getConfig("abc", null, process.getProcess()).getAccessTokenSigningKeyDynamic(), -// false); -// -// process.kill(); -// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); -// } // // @Test // public void testDifferentWaysToGetConfigBasedOnConnectionURIAndTenantId() From a700b7204630b9ce0b89e89f52d02f2dda9664e2 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Wed, 25 Jan 2023 15:56:31 +0530 Subject: [PATCH 012/106] adds more tests and changes config parsing to prioritise connection uri input --- .../postgresql/config/PostgreSQLConfig.java | 102 +++++---- .../test/multitenancy/StorageLayerTest.java | 206 +++++++++++------- 2 files changed, 184 insertions(+), 124 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java index 2dda5a1a..a1e3cf40 100644 --- a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java +++ b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java @@ -78,6 +78,17 @@ public class PostgreSQLConfig { private String postgresql_connection_uri = null; public String getTableSchema() { + if (postgresql_connection_uri != null) { + String connectionAttributes = getConnectionAttributes(); + if (connectionAttributes.contains("currentSchema=")) { + String[] splitted = connectionAttributes.split("currentSchema="); + String valueStr = splitted[1]; + if (valueStr.contains("&")) { + return valueStr.split("&")[0]; + } + return valueStr; + } + } return postgresql_table_schema; } @@ -115,78 +126,73 @@ public String getConnectionAttributes() { } public String getHostName() { - if (postgresql_host == null) { - if (postgresql_connection_uri != null) { - URI uri = URI.create(postgresql_connection_uri); - if (uri.getHost() != null) { - return uri.getHost(); - } + if (postgresql_connection_uri != null) { + URI uri = URI.create(postgresql_connection_uri); + if (uri.getHost() != null) { + return uri.getHost(); } - return "localhost"; + } else if (postgresql_host != null) { + return postgresql_host; } - return postgresql_host; + return "localhost"; } public int getPort() { - if (postgresql_port == -1) { - if (postgresql_connection_uri != null) { - URI uri = URI.create(postgresql_connection_uri); - return uri.getPort(); - } - return 5432; + if (postgresql_connection_uri != null) { + URI uri = URI.create(postgresql_connection_uri); + return uri.getPort(); + } else if (postgresql_port != -1) { + return postgresql_port; } - return postgresql_port; + return 5432; } public String getUser() { - if (postgresql_user == null) { - if (postgresql_connection_uri != null) { - URI uri = URI.create(postgresql_connection_uri); - String userInfo = uri.getUserInfo(); - if (userInfo != null) { - String[] userInfoArray = userInfo.split(":"); - if (userInfoArray.length > 0 && !userInfoArray[0].equals("")) { - return userInfoArray[0]; - } + if (postgresql_connection_uri != null) { + URI uri = URI.create(postgresql_connection_uri); + String userInfo = uri.getUserInfo(); + if (userInfo != null) { + String[] userInfoArray = userInfo.split(":"); + if (userInfoArray.length > 0 && !userInfoArray[0].equals("")) { + return userInfoArray[0]; } } - return null; + } else if (postgresql_user != null) { + return postgresql_user; } - return postgresql_user; + return null; } public String getPassword() { - if (postgresql_password == null) { - if (postgresql_connection_uri != null) { - URI uri = URI.create(postgresql_connection_uri); - String userInfo = uri.getUserInfo(); - if (userInfo != null) { - String[] userInfoArray = userInfo.split(":"); - if (userInfoArray.length > 1 && !userInfoArray[1].equals("")) { - return userInfoArray[1]; - } + if (postgresql_connection_uri != null) { + URI uri = URI.create(postgresql_connection_uri); + String userInfo = uri.getUserInfo(); + if (userInfo != null) { + String[] userInfoArray = userInfo.split(":"); + if (userInfoArray.length > 1 && !userInfoArray[1].equals("")) { + return userInfoArray[1]; } } - return null; + } else if (postgresql_password != null) { + return postgresql_password; } - return postgresql_password; + return null; } public String getDatabaseName() { - if (postgresql_database_name == null) { - if (postgresql_connection_uri != null) { - URI uri = URI.create(postgresql_connection_uri); - String path = uri.getPath(); - if (path != null && !path.equals("") && !path.equals("/")) { - if (path.startsWith("/")) { - return path.substring(1); - } - return path; + if (postgresql_connection_uri != null) { + URI uri = URI.create(postgresql_connection_uri); + String path = uri.getPath(); + if (path != null && !path.equals("") && !path.equals("/")) { + if (path.startsWith("/")) { + return path.substring(1); } + return path; } - return "supertokens"; + } else if (postgresql_database_name != null) { + return postgresql_database_name; } - return postgresql_database_name; + return "supertokens"; } public String getConnectionURI() { diff --git a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java index 89b0aa44..e3ec893a 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java @@ -441,80 +441,134 @@ public void newStorageIsNotCreatedWhenSameTenantIsAdded() process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } -// -// @Test -// public void testDifferentWaysToGetConfigBasedOnConnectionURIAndTenantId() -// throws InterruptedException, IOException, InvalidConfigException { -// String[] args = {"../"}; -// -// Utils.setValueInConfig("refresh_token_validity", "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)); -// -// TenantConfig[] tenants = new TenantConfig[4]; -// -// { -// JsonObject tenantConfig = new JsonObject(); -// tenantConfig.add("refresh_token_validity", new JsonPrimitive(144002)); -// tenants[0] = new TenantConfig("c1", null, new EmailPasswordConfig(false), -// new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), -// new PasswordlessConfig(false), -// tenantConfig); -// } -// -// { -// JsonObject tenantConfig = new JsonObject(); -// tenantConfig.add("refresh_token_validity", new JsonPrimitive(144003)); -// tenants[1] = new TenantConfig("c1", "t1", new EmailPasswordConfig(false), -// new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), -// new PasswordlessConfig(false), -// tenantConfig); -// } -// -// { -// JsonObject tenantConfig = new JsonObject(); -// tenantConfig.add("refresh_token_validity", new JsonPrimitive(144004)); -// tenants[2] = new TenantConfig(null, "t2", new EmailPasswordConfig(false), -// new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), -// new PasswordlessConfig(false), -// tenantConfig); -// } -// -// { -// JsonObject tenantConfig = new JsonObject(); -// tenantConfig.add("refresh_token_validity", new JsonPrimitive(144005)); -// tenants[3] = new TenantConfig(null, "t1", new EmailPasswordConfig(false), -// new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), -// new PasswordlessConfig(false), -// tenantConfig); -// } -// -// Config.loadAllTenantConfig(process.getProcess(), tenants); -// -// Assert.assertEquals(Config.getConfig(null, null, process.getProcess()).getRefreshTokenValidity(), -// (long) 144001 * 60 * 1000); -// Assert.assertEquals(Config.getConfig("c1", null, process.getProcess()).getRefreshTokenValidity(), -// (long) 144002 * 60 * 1000); -// Assert.assertEquals(Config.getConfig("c1", "t1", process.getProcess()).getRefreshTokenValidity(), -// (long) 144003 * 60 * 1000); -// Assert.assertEquals(Config.getConfig(null, "t1", process.getProcess()).getRefreshTokenValidity(), -// (long) 144005 * 60 * 1000); -// Assert.assertEquals(Config.getConfig("c2", null, process.getProcess()).getRefreshTokenValidity(), -// (long) 144001 * 60 * 1000); -// Assert.assertEquals(Config.getConfig("c2", "t1", process.getProcess()).getRefreshTokenValidity(), -// (long) 144005 * 60 * 1000); -// Assert.assertEquals(Config.getConfig("c3", "t2", process.getProcess()).getRefreshTokenValidity(), -// (long) 144004 * 60 * 1000); -// Assert.assertEquals(Config.getConfig("c1", "t2", process.getProcess()).getRefreshTokenValidity(), -// (long) 144002 * 60 * 1000); -// Assert.assertEquals(Config.getConfig(null, "t2", process.getProcess()).getRefreshTokenValidity(), -// (long) 144004 * 60 * 1000); -// -// -// process.kill(); -// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); -// } + + @Test + public void testDifferentWaysToGetConfigBasedOnConnectionURIAndTenantId() + throws InterruptedException, IOException, InvalidConfigException, DbInitException { + 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)); + + TenantConfig[] tenants = new TenantConfig[4]; + + { + JsonObject tenantConfig = new JsonObject(); + tenantConfig.add("postgresql_connection_pool_size", new JsonPrimitive(12)); + tenants[0] = new TenantConfig("c1", null, new EmailPasswordConfig(false), + new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), + new PasswordlessConfig(false), + tenantConfig); + } + + { + JsonObject tenantConfig = new JsonObject(); + tenantConfig.add("postgresql_connection_pool_size", new JsonPrimitive(13)); + tenants[1] = new TenantConfig("c1", "t1", new EmailPasswordConfig(false), + new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), + new PasswordlessConfig(false), + tenantConfig); + } + + { + JsonObject tenantConfig = new JsonObject(); + tenantConfig.add("postgresql_connection_pool_size", new JsonPrimitive(14)); + tenants[2] = new TenantConfig(null, "t2", new EmailPasswordConfig(false), + new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), + new PasswordlessConfig(false), + tenantConfig); + } + + { + JsonObject tenantConfig = new JsonObject(); + tenantConfig.add("postgresql_connection_pool_size", new JsonPrimitive(15)); + tenants[3] = new TenantConfig(null, "t1", new EmailPasswordConfig(false), + new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), + new PasswordlessConfig(false), + tenantConfig); + } + + Config.loadAllTenantConfig(process.getProcess(), tenants); + + StorageLayer.loadAllTenantStorage(process.getProcess(), tenants); + + Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( + (Start) StorageLayer.getStorage(null, null, process.getProcess())) + .getConnectionPoolSize(), 10); + + Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( + (Start) StorageLayer.getStorage("c1", null, process.getProcess())) + .getConnectionPoolSize(), 12); + + Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( + (Start) StorageLayer.getStorage("c1", "t1", process.getProcess())) + .getConnectionPoolSize(), 13); + + Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( + (Start) StorageLayer.getStorage(null, "t1", process.getProcess())) + .getConnectionPoolSize(), 15); + + Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( + (Start) StorageLayer.getStorage("c2", null, process.getProcess())) + .getConnectionPoolSize(), 10); + + Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( + (Start) StorageLayer.getStorage("c1", "t1", process.getProcess())) + .getConnectionPoolSize(), 13); + + Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( + (Start) StorageLayer.getStorage("c3", "t2", process.getProcess())) + .getConnectionPoolSize(), 14); + + Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( + (Start) StorageLayer.getStorage("c1", "t2", process.getProcess())) + .getConnectionPoolSize(), 12); + + Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( + (Start) StorageLayer.getStorage(null, "t2", process.getProcess())) + .getConnectionPoolSize(), 14); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void differentUserPoolCreatedBasedOnConnectionUri() + throws InterruptedException, IOException, InvalidConfigException { + 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)); + + JsonObject tenantConfig = new JsonObject(); + tenantConfig.add("postgresql_connection_uri", + new JsonPrimitive("postgresql://root:root@localhost:5432/random")); + + try { + TenantConfig[] tenants = new TenantConfig[]{ + new TenantConfig("abc", null, new EmailPasswordConfig(false), + new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), + new PasswordlessConfig(false), + tenantConfig)}; + Config.loadAllTenantConfig(process.getProcess(), tenants); + + StorageLayer.loadAllTenantStorage(process.getProcess(), tenants); + fail(); + } catch (DbInitException e) { + assertEquals(e.getMessage(), "com.zaxxer.hikari.pool.HikariPool$PoolInitializationException: Failed to " + + "initialize pool: FATAL: database \"random\" does not exist"); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + // TODO: different connection URI creates different connection pool -> based on schema (via connenction uri and + // otherwise) difference (should work). } From 91b3e33c6704f38f0e1de05b324e7173d2244953 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Wed, 25 Jan 2023 17:52:24 +0530 Subject: [PATCH 013/106] fixes a few config parsing bugs --- .../storage/postgresql/config/Config.java | 2 +- .../postgresql/config/PostgreSQLConfig.java | 24 ++++++++++++++----- .../storage/postgresql/test/ConfigTest.java | 2 +- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/config/Config.java b/src/main/java/io/supertokens/storage/postgresql/config/Config.java index 87cf5f0c..c6f456b0 100644 --- a/src/main/java/io/supertokens/storage/postgresql/config/Config.java +++ b/src/main/java/io/supertokens/storage/postgresql/config/Config.java @@ -109,7 +109,7 @@ public static boolean canBeUsed(JsonObject configJson) { try { final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); PostgreSQLConfig config = mapper.readValue(configJson.toString(), PostgreSQLConfig.class); - return config.getUser() != null || config.getPassword() != null || config.getConnectionURI() != null; + return config.getConnectionURI() != null || config.getUser() != null || config.getPassword() != null; } catch (Exception e) { return false; } diff --git a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java index a1e3cf40..3af3ba6f 100644 --- a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java +++ b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java @@ -131,7 +131,9 @@ public String getHostName() { if (uri.getHost() != null) { return uri.getHost(); } - } else if (postgresql_host != null) { + } + + if (postgresql_host != null) { return postgresql_host; } return "localhost"; @@ -140,8 +142,12 @@ public String getHostName() { public int getPort() { if (postgresql_connection_uri != null) { URI uri = URI.create(postgresql_connection_uri); - return uri.getPort(); - } else if (postgresql_port != -1) { + if (uri.getPort() > 0) { + return uri.getPort(); + } + } + + if (postgresql_port != -1) { return postgresql_port; } return 5432; @@ -157,7 +163,9 @@ public String getUser() { return userInfoArray[0]; } } - } else if (postgresql_user != null) { + } + + if (postgresql_user != null) { return postgresql_user; } return null; @@ -173,7 +181,9 @@ public String getPassword() { return userInfoArray[1]; } } - } else if (postgresql_password != null) { + } + + if (postgresql_password != null) { return postgresql_password; } return null; @@ -189,7 +199,9 @@ public String getDatabaseName() { } return path; } - } else if (postgresql_database_name != null) { + } + + if (postgresql_database_name != null) { return postgresql_database_name; } return "supertokens"; diff --git a/src/test/java/io/supertokens/storage/postgresql/test/ConfigTest.java b/src/test/java/io/supertokens/storage/postgresql/test/ConfigTest.java index 4e850f2b..32ebfcdf 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/ConfigTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/ConfigTest.java @@ -359,7 +359,7 @@ public void testValidConnectionURI() throws Exception { TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); PostgreSQLConfig config = Config.getConfig((Start) StorageLayer.getStorage(process.getProcess())); - assertEquals(config.getPort(), -1); + assertEquals(config.getPort(), 5432); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); From 4ce4383b2c95f1917daa520853e0351cdb2279e7 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Wed, 25 Jan 2023 20:02:44 +0530 Subject: [PATCH 014/106] adds more tests --- .../postgresql/config/PostgreSQLConfig.java | 8 +- .../storage/postgresql/test/ConfigTest.java | 129 +++++++++++++++++ .../test/multitenancy/StorageLayerTest.java | 131 +++++++++++++++++- 3 files changed, 262 insertions(+), 6 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java index 3af3ba6f..b3d6ca74 100644 --- a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java +++ b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java @@ -86,10 +86,10 @@ public String getTableSchema() { if (valueStr.contains("&")) { return valueStr.split("&")[0]; } - return valueStr; + return valueStr.trim(); } } - return postgresql_table_schema; + return postgresql_table_schema.trim(); } public int getConnectionPoolSize() { @@ -329,8 +329,8 @@ private String addSchemaAndPrefixToTableName(String tableName) { private String addSchemaToTableName(String tableName) { String name = tableName; - if (!postgresql_table_schema.trim().equals("public")) { - name = postgresql_table_schema.trim() + "." + name; + if (!getTableSchema().equals("public")) { + name = getTableSchema() + "." + name; } return name; } diff --git a/src/test/java/io/supertokens/storage/postgresql/test/ConfigTest.java b/src/test/java/io/supertokens/storage/postgresql/test/ConfigTest.java index 32ebfcdf..14ef81bc 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/ConfigTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/ConfigTest.java @@ -320,6 +320,135 @@ public void testAddingSchemaWorks() throws Exception { TestingProcessManager.deleteAllInformation(); } + @Test + public void testAddingSchemaViaConnectionUriWorks() throws Exception { + String[] args = {"../"}; + + Utils.setValueInConfig("postgresql_connection_uri", + "postgresql://root:root@localhost:5432/supertokens?currentSchema=myschema"); + Utils.setValueInConfig("postgresql_table_names_prefix", "some_prefix"); + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + PostgreSQLConfig config = Config.getConfig((Start) StorageLayer.getStorage(process.getProcess())); + + assertEquals("change in KeyValueTable name not reflected", config.getKeyValueTable(), + "myschema.some_prefix_key_value"); + assertEquals("change in SessionInfoTable name not reflected", config.getSessionInfoTable(), + "myschema.some_prefix_session_info"); + assertEquals("change in table name not reflected", config.getEmailPasswordUsersTable(), + "myschema.some_prefix_emailpassword_users"); + assertEquals("change in table name not reflected", config.getPasswordResetTokensTable(), + "myschema.some_prefix_emailpassword_pswd_reset_tokens"); + + String userId = "userId"; + JsonObject userDataInJWT = new JsonObject(); + userDataInJWT.addProperty("key", "value"); + JsonObject userDataInDatabase = new JsonObject(); + userDataInDatabase.addProperty("key", "value"); + + SessionInformationHolder sessionInfo = Session.createNewSession(process.getProcess(), userId, userDataInJWT, + userDataInDatabase); + + assert sessionInfo.accessToken != null; + assert sessionInfo.refreshToken != null; + + TestCase.assertEquals(StorageLayer.getSessionStorage(process.getProcess()).getNumberOfSessions(), 1); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + + // we call this here so that the database is cleared with the modified table names + // since in postgres, we delete all dbs one by one + TestingProcessManager.deleteAllInformation(); + } + + @Test + public void testAddingSchemaViaConnectionUriWorks2() throws Exception { + String[] args = {"../"}; + + Utils.setValueInConfig("postgresql_connection_uri", + "postgresql://root:root@localhost:5432/supertokens?a=b¤tSchema=myschema"); + Utils.setValueInConfig("postgresql_table_names_prefix", "some_prefix"); + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + PostgreSQLConfig config = Config.getConfig((Start) StorageLayer.getStorage(process.getProcess())); + + assertEquals("change in KeyValueTable name not reflected", config.getKeyValueTable(), + "myschema.some_prefix_key_value"); + assertEquals("change in SessionInfoTable name not reflected", config.getSessionInfoTable(), + "myschema.some_prefix_session_info"); + assertEquals("change in table name not reflected", config.getEmailPasswordUsersTable(), + "myschema.some_prefix_emailpassword_users"); + assertEquals("change in table name not reflected", config.getPasswordResetTokensTable(), + "myschema.some_prefix_emailpassword_pswd_reset_tokens"); + + String userId = "userId"; + JsonObject userDataInJWT = new JsonObject(); + userDataInJWT.addProperty("key", "value"); + JsonObject userDataInDatabase = new JsonObject(); + userDataInDatabase.addProperty("key", "value"); + + SessionInformationHolder sessionInfo = Session.createNewSession(process.getProcess(), userId, userDataInJWT, + userDataInDatabase); + + assert sessionInfo.accessToken != null; + assert sessionInfo.refreshToken != null; + + TestCase.assertEquals(StorageLayer.getSessionStorage(process.getProcess()).getNumberOfSessions(), 1); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + + // we call this here so that the database is cleared with the modified table names + // since in postgres, we delete all dbs one by one + TestingProcessManager.deleteAllInformation(); + } + + @Test + public void testAddingSchemaViaConnectionUriWorks3() throws Exception { + String[] args = {"../"}; + + Utils.setValueInConfig("postgresql_connection_uri", + "postgresql://root:root@localhost:5432/supertokens?e=f¤tSchema=myschema&a=b&c=d"); + Utils.setValueInConfig("postgresql_table_names_prefix", "some_prefix"); + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + PostgreSQLConfig config = Config.getConfig((Start) StorageLayer.getStorage(process.getProcess())); + + assertEquals("change in KeyValueTable name not reflected", config.getKeyValueTable(), + "myschema.some_prefix_key_value"); + assertEquals("change in SessionInfoTable name not reflected", config.getSessionInfoTable(), + "myschema.some_prefix_session_info"); + assertEquals("change in table name not reflected", config.getEmailPasswordUsersTable(), + "myschema.some_prefix_emailpassword_users"); + assertEquals("change in table name not reflected", config.getPasswordResetTokensTable(), + "myschema.some_prefix_emailpassword_pswd_reset_tokens"); + + String userId = "userId"; + JsonObject userDataInJWT = new JsonObject(); + userDataInJWT.addProperty("key", "value"); + JsonObject userDataInDatabase = new JsonObject(); + userDataInDatabase.addProperty("key", "value"); + + SessionInformationHolder sessionInfo = Session.createNewSession(process.getProcess(), userId, userDataInJWT, + userDataInDatabase); + + assert sessionInfo.accessToken != null; + assert sessionInfo.refreshToken != null; + + TestCase.assertEquals(StorageLayer.getSessionStorage(process.getProcess()).getNumberOfSessions(), 1); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + + // we call this here so that the database is cleared with the modified table names + // since in postgres, we delete all dbs one by one + TestingProcessManager.deleteAllInformation(); + } + @Test public void testValidConnectionURI() throws Exception { final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); diff --git a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java index e3ec893a..a728606e 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java @@ -396,6 +396,26 @@ public void mergingDifferentUserPoolIdTenantWithBaseConfigWithConflictingConfigs StorageLayer.loadAllTenantStorage(process.getProcess(), tenants); + Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( + (Start) StorageLayer.getStorage("abc", null, process.getProcess())) + .getTableSchema(), "supertokens2"); + Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( + (Start) StorageLayer.getStorage("abc", null, process.getProcess())) + .getConnectionPoolSize(), 11); + Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( + (Start) StorageLayer.getStorage("abc", null, process.getProcess())) + .getThirdPartyUsersTable(), "supertokens2.random"); + + Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( + (Start) StorageLayer.getStorage(null, null, process.getProcess())) + .getTableSchema(), "public"); + Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( + (Start) StorageLayer.getStorage(null, null, process.getProcess())) + .getConnectionPoolSize(), 10); + Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( + (Start) StorageLayer.getStorage(null, null, process.getProcess())) + .getThirdPartyUsersTable(), "thirdparty_users"); + process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } @@ -569,6 +589,113 @@ public void differentUserPoolCreatedBasedOnConnectionUri() assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } - // TODO: different connection URI creates different connection pool -> based on schema (via connenction uri and - // otherwise) difference (should work). + @Test + public void differentUserPoolCreatedBasedOnSchemaInConnectionUri() + throws InterruptedException, IOException, InvalidConfigException, DbInitException { + 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)); + + JsonObject tenantConfig = new JsonObject(); + tenantConfig.add("postgresql_connection_uri", + new JsonPrimitive("postgresql://root:root@localhost:5432/supertokens?currentSchema=random")); + + TenantConfig[] tenants = new TenantConfig[]{ + new TenantConfig("abc", null, new EmailPasswordConfig(false), + new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), + new PasswordlessConfig(false), + tenantConfig)}; + Config.loadAllTenantConfig(process.getProcess(), tenants); + + StorageLayer.loadAllTenantStorage(process.getProcess(), tenants); + + Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( + (Start) StorageLayer.getStorage("abc", null, process.getProcess())) + .getTableSchema(), "random"); + + Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( + (Start) StorageLayer.getStorage(null, null, process.getProcess())) + .getTableSchema(), "public"); + + assertNotSame(StorageLayer.getStorage("abc", null, process.getProcess()), + StorageLayer.getStorage(null, null, process.getProcess())); + + Assert.assertEquals( + process.getProcess().getResourceDistributor().getAllResourcesWithResourceKey(StorageLayer.RESOURCE_KEY) + .size(), 2); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void multipleTenantsSameUserPoolAndConnectionPoolShouldWork() + throws InterruptedException, IOException, InvalidConfigException, DbInitException { + 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)); + + JsonObject tenantConfig = new JsonObject(); + + TenantConfig[] tenants = new TenantConfig[]{ + new TenantConfig("abc", null, new EmailPasswordConfig(false), + new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), + new PasswordlessConfig(false), + tenantConfig)}; + Config.loadAllTenantConfig(process.getProcess(), tenants); + + StorageLayer.loadAllTenantStorage(process.getProcess(), tenants); + + assertSame(StorageLayer.getStorage("abc", null, process.getProcess()), + StorageLayer.getStorage(null, null, process.getProcess())); + + Assert.assertEquals( + process.getProcess().getResourceDistributor().getAllResourcesWithResourceKey(StorageLayer.RESOURCE_KEY) + .size(), 2); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void multipleTenantsSameUserPoolAndDifferentConnectionPoolShouldWork() + throws InterruptedException, IOException, InvalidConfigException, DbInitException { + 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)); + + JsonObject tenantConfig = new JsonObject(); + tenantConfig.add("postgresql_connection_pool_size", new JsonPrimitive(20)); + + TenantConfig[] tenants = new TenantConfig[]{ + new TenantConfig("abc", null, new EmailPasswordConfig(false), + new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), + new PasswordlessConfig(false), + tenantConfig)}; + Config.loadAllTenantConfig(process.getProcess(), tenants); + + StorageLayer.loadAllTenantStorage(process.getProcess(), tenants); + + assertNotSame(StorageLayer.getStorage("abc", null, process.getProcess()), + StorageLayer.getStorage(null, null, process.getProcess())); + + Assert.assertEquals( + process.getProcess().getResourceDistributor().getAllResourcesWithResourceKey(StorageLayer.RESOURCE_KEY) + .size(), 2); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } } From b68aebbc7d3254efd0cc99361ed9dc48ce2a0c2f Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Sat, 28 Jan 2023 14:37:07 +0530 Subject: [PATCH 015/106] modifies testing to clear multiple user pools after each test --- .../io/supertokens/storage/postgresql/Start.java | 6 ++++++ .../postgresql/test/TestingProcessManager.java | 14 ++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 7ee9a3cd..68424706 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -19,6 +19,7 @@ import ch.qos.logback.classic.Logger; import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; import io.supertokens.pluginInterface.KeyValueInfo; import io.supertokens.pluginInterface.LOG_LEVEL; import io.supertokens.pluginInterface.RECIPE_ID; @@ -651,6 +652,11 @@ public void addInfoToNonAuthRecipesBasedOnUserId(String className, String userId } } + @Override + public void modifyConfigToAddANewUserPoolForTesting(JsonObject config, int poolNumber) { + config.add("postgresql_database_name", new JsonPrimitive("st" + poolNumber)); + } + @Override public void signUp(UserInfo userInfo) throws StorageQueryException, DuplicateUserIdException, DuplicateEmailException { diff --git a/src/test/java/io/supertokens/storage/postgresql/test/TestingProcessManager.java b/src/test/java/io/supertokens/storage/postgresql/test/TestingProcessManager.java index 72b080d4..a34ff61f 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/TestingProcessManager.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/TestingProcessManager.java @@ -133,6 +133,20 @@ public void kill() throws InterruptedException { if (killed) { return; } + // we check if there are multiple user pool IDs loaded, and if there are, + // we clear all the info before killing cause otherwise those extra dbs will retain info + // across tests + if (StorageLayer.hasMultipleUserPools(this.main)) { + try { + main.deleteAllInformationForTesting(); + } catch (Exception e) { + if (!e.getMessage().contains("Please call initPool before getConnection")) { + // we ignore this type of message because it's due to tests in which the init failed + // and here we try and delete assuming that init had succeeded. + throw new RuntimeException(e); + } + } + } main.killForTestingAndWaitForShutdown(); killed = true; } From 55e8075da9d211e8f88e10a251d865f376956c56 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 31 Jan 2023 15:27:35 +0530 Subject: [PATCH 016/106] makes initlogging idempotent --- src/main/java/io/supertokens/storage/postgresql/Start.java | 3 +++ .../io/supertokens/storage/postgresql/output/Logging.java | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 68424706..a662f96c 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -147,6 +147,9 @@ public void assertThatConfigFromSameUserPoolIsNotConflicting(JsonObject otherCon @Override public void initFileLogging(String infoLogPath, String errorLogPath) { + if (Logging.isAlreadyInitialised(this)) { + return; + } Logging.initFileLogging(this, infoLogPath, errorLogPath); /* diff --git a/src/main/java/io/supertokens/storage/postgresql/output/Logging.java b/src/main/java/io/supertokens/storage/postgresql/output/Logging.java index 7e173019..7b59ba5c 100644 --- a/src/main/java/io/supertokens/storage/postgresql/output/Logging.java +++ b/src/main/java/io/supertokens/storage/postgresql/output/Logging.java @@ -48,6 +48,10 @@ private static Logging getInstance(Start start) { return (Logging) start.getResourceDistributor().getResource(RESOURCE_ID); } + public static boolean isAlreadyInitialised(Start start) { + return getInstance(start) != null; + } + public static void initFileLogging(Start start, String infoLogPath, String errorLogPath) { if (getInstance(start) == null) { start.getResourceDistributor().setResource(RESOURCE_ID, new Logging(start, infoLogPath, errorLogPath)); From cbc005b55c10da8cbdea73162f088c34b702ce95 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Fri, 3 Feb 2023 12:14:16 +0530 Subject: [PATCH 017/106] fixes all tests --- .../test/multitenancy/StorageLayerTest.java | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java index a728606e..6eec2127 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java @@ -20,6 +20,7 @@ import com.google.gson.JsonPrimitive; import io.supertokens.ProcessState; import io.supertokens.config.Config; +import io.supertokens.exceptions.TenantNotFoundException; import io.supertokens.featureflag.EE_FEATURES; import io.supertokens.featureflag.FeatureFlagTestContent; import io.supertokens.pluginInterface.Storage; @@ -77,7 +78,7 @@ public void normalDbConfigErrorContinuesToWork() throws InterruptedException, IO @Test public void mergingTenantWithBaseConfigWorks() - throws InterruptedException, IOException, InvalidConfigException, DbInitException { + throws InterruptedException, IOException, InvalidConfigException, DbInitException, TenantNotFoundException { String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); @@ -162,7 +163,7 @@ public void creatingTenantWithNoExistingDbThrowsError() @Test public void storageInstanceIsReusedAcrossTenants() - throws InterruptedException, IOException, InvalidConfigException, DbInitException { + throws InterruptedException, IOException, InvalidConfigException, DbInitException, TenantNotFoundException { String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); @@ -203,7 +204,7 @@ public void storageInstanceIsReusedAcrossTenants() @Test public void storageInstanceIsReusedAcrossTenantsComplex() - throws InterruptedException, IOException, InvalidConfigException, DbInitException { + throws InterruptedException, IOException, InvalidConfigException, DbInitException, TenantNotFoundException { String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); @@ -373,7 +374,7 @@ public void mergingDifferentConnectionPoolIdTenantWithBaseConfigWithConflictingC @Test public void mergingDifferentUserPoolIdTenantWithBaseConfigWithConflictingConfigsShouldNotThrowsError() - throws InterruptedException, IOException, InvalidConfigException, DbInitException { + throws InterruptedException, IOException, InvalidConfigException, DbInitException, TenantNotFoundException { String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); @@ -422,7 +423,7 @@ public void mergingDifferentUserPoolIdTenantWithBaseConfigWithConflictingConfigs @Test public void newStorageIsNotCreatedWhenSameTenantIsAdded() - throws InterruptedException, IOException, InvalidConfigException, DbInitException { + throws InterruptedException, IOException, InvalidConfigException, DbInitException, TenantNotFoundException { String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); @@ -464,7 +465,7 @@ public void newStorageIsNotCreatedWhenSameTenantIsAdded() @Test public void testDifferentWaysToGetConfigBasedOnConnectionURIAndTenantId() - throws InterruptedException, IOException, InvalidConfigException, DbInitException { + throws InterruptedException, IOException, InvalidConfigException, DbInitException, TenantNotFoundException { String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); @@ -543,10 +544,6 @@ public void testDifferentWaysToGetConfigBasedOnConnectionURIAndTenantId() (Start) StorageLayer.getStorage("c3", "t2", process.getProcess())) .getConnectionPoolSize(), 14); - Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( - (Start) StorageLayer.getStorage("c1", "t2", process.getProcess())) - .getConnectionPoolSize(), 12); - Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( (Start) StorageLayer.getStorage(null, "t2", process.getProcess())) .getConnectionPoolSize(), 14); @@ -591,7 +588,7 @@ public void differentUserPoolCreatedBasedOnConnectionUri() @Test public void differentUserPoolCreatedBasedOnSchemaInConnectionUri() - throws InterruptedException, IOException, InvalidConfigException, DbInitException { + throws InterruptedException, IOException, InvalidConfigException, DbInitException, TenantNotFoundException { String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); @@ -634,7 +631,7 @@ public void differentUserPoolCreatedBasedOnSchemaInConnectionUri() @Test public void multipleTenantsSameUserPoolAndConnectionPoolShouldWork() - throws InterruptedException, IOException, InvalidConfigException, DbInitException { + throws InterruptedException, IOException, InvalidConfigException, DbInitException, TenantNotFoundException { String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); @@ -667,7 +664,7 @@ public void multipleTenantsSameUserPoolAndConnectionPoolShouldWork() @Test public void multipleTenantsSameUserPoolAndDifferentConnectionPoolShouldWork() - throws InterruptedException, IOException, InvalidConfigException, DbInitException { + throws InterruptedException, IOException, InvalidConfigException, DbInitException, TenantNotFoundException { String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); From ccb6f733b9d93daef039c0df35710543203df51e Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Sun, 5 Feb 2023 20:27:21 +0530 Subject: [PATCH 018/106] fixes tests --- .../test/multitenancy/StorageLayerTest.java | 175 ++++++++++-------- 1 file changed, 95 insertions(+), 80 deletions(-) diff --git a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java index 6eec2127..c824737a 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java @@ -20,16 +20,13 @@ import com.google.gson.JsonPrimitive; import io.supertokens.ProcessState; import io.supertokens.config.Config; -import io.supertokens.exceptions.TenantNotFoundException; +import io.supertokens.exceptions.TenantOrAppNotFoundException; import io.supertokens.featureflag.EE_FEATURES; import io.supertokens.featureflag.FeatureFlagTestContent; import io.supertokens.pluginInterface.Storage; import io.supertokens.pluginInterface.exceptions.DbInitException; import io.supertokens.pluginInterface.exceptions.InvalidConfigException; -import io.supertokens.pluginInterface.multitenancy.EmailPasswordConfig; -import io.supertokens.pluginInterface.multitenancy.PasswordlessConfig; -import io.supertokens.pluginInterface.multitenancy.TenantConfig; -import io.supertokens.pluginInterface.multitenancy.ThirdPartyConfig; +import io.supertokens.pluginInterface.multitenancy.*; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.test.TestingProcessManager; import io.supertokens.storage.postgresql.test.Utils; @@ -78,7 +75,8 @@ public void normalDbConfigErrorContinuesToWork() throws InterruptedException, IO @Test public void mergingTenantWithBaseConfigWorks() - throws InterruptedException, IOException, InvalidConfigException, DbInitException, TenantNotFoundException { + throws InterruptedException, IOException, InvalidConfigException, DbInitException, + TenantOrAppNotFoundException { String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); @@ -92,7 +90,7 @@ public void mergingTenantWithBaseConfigWorks() tenantConfig.add("postgresql_table_schema", new JsonPrimitive("random")); TenantConfig[] tenants = new TenantConfig[]{ - new TenantConfig("abc", null, new EmailPasswordConfig(false), + new TenantConfig(new TenantIdentifier("abc", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), tenantConfig)}; @@ -101,21 +99,21 @@ public void mergingTenantWithBaseConfigWorks() StorageLayer.loadAllTenantStorage(process.getProcess(), tenants); - assertNotSame(StorageLayer.getStorage("abc", null, process.getProcess()), - StorageLayer.getStorage(null, null, process.getProcess())); + assertNotSame(StorageLayer.getStorage(new TenantIdentifier("abc", null, null), process.getProcess()), + StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess())); Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( - (Start) StorageLayer.getStorage(null, null, process.getProcess())) + (Start) StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess())) .getTablePrefix(), ""); Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( - (Start) StorageLayer.getStorage(null, null, process.getProcess())) + (Start) StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess())) .getTableSchema(), "public"); Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( - (Start) StorageLayer.getStorage("abc", null, process.getProcess())) + (Start) StorageLayer.getStorage(new TenantIdentifier("abc", null, null), process.getProcess())) .getTablePrefix(), "test"); Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( - (Start) StorageLayer.getStorage("abc", null, process.getProcess())) + (Start) StorageLayer.getStorage(new TenantIdentifier("abc", null, null), process.getProcess())) .getTableSchema(), "random"); Assert.assertEquals( @@ -143,7 +141,7 @@ public void creatingTenantWithNoExistingDbThrowsError() try { TenantConfig[] tenants = new TenantConfig[]{ - new TenantConfig("abc", null, new EmailPasswordConfig(false), + new TenantConfig(new TenantIdentifier("abc", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), tenantConfig)}; @@ -163,7 +161,8 @@ public void creatingTenantWithNoExistingDbThrowsError() @Test public void storageInstanceIsReusedAcrossTenants() - throws InterruptedException, IOException, InvalidConfigException, DbInitException, TenantNotFoundException { + throws InterruptedException, IOException, InvalidConfigException, DbInitException, + TenantOrAppNotFoundException { String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); @@ -176,7 +175,7 @@ public void storageInstanceIsReusedAcrossTenants() tenantConfig.add("access_token_validity", new JsonPrimitive(3601)); TenantConfig[] tenants = new TenantConfig[]{ - new TenantConfig("abc", null, new EmailPasswordConfig(false), + new TenantConfig(new TenantIdentifier("abc", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), tenantConfig)}; @@ -185,13 +184,15 @@ public void storageInstanceIsReusedAcrossTenants() StorageLayer.loadAllTenantStorage(process.getProcess(), tenants); - assertSame(StorageLayer.getStorage("abc", null, process.getProcess()), - StorageLayer.getStorage(null, null, process.getProcess())); + assertSame(StorageLayer.getStorage(new TenantIdentifier("abc", null, null), process.getProcess()), + StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess())); - Assert.assertEquals(Config.getConfig(null, null, process.getProcess()).getAccessTokenValidity(), + Assert.assertEquals( + Config.getConfig(new TenantIdentifier(null, null, null), process.getProcess()).getAccessTokenValidity(), (long) 3600 * 1000); - Assert.assertEquals(Config.getConfig("abc", null, process.getProcess()).getAccessTokenValidity(), + Assert.assertEquals(Config.getConfig(new TenantIdentifier("abc", null, null), process.getProcess()) + .getAccessTokenValidity(), (long) 3601 * 1000); Assert.assertEquals( @@ -204,7 +205,8 @@ public void storageInstanceIsReusedAcrossTenants() @Test public void storageInstanceIsReusedAcrossTenantsComplex() - throws InterruptedException, IOException, InvalidConfigException, DbInitException, TenantNotFoundException { + throws InterruptedException, IOException, InvalidConfigException, DbInitException, + TenantOrAppNotFoundException { String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); @@ -220,15 +222,15 @@ public void storageInstanceIsReusedAcrossTenantsComplex() tenantConfig1.add("postgresql_connection_pool_size", new JsonPrimitive(11)); TenantConfig[] tenants = new TenantConfig[]{ - new TenantConfig("abc", null, new EmailPasswordConfig(false), + new TenantConfig(new TenantIdentifier("abc", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), tenantConfig), - new TenantConfig("abc", "t1", new EmailPasswordConfig(false), + new TenantConfig(new TenantIdentifier("abc", null, "t1"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), tenantConfig1), - new TenantConfig(null, "t2", new EmailPasswordConfig(false), + new TenantConfig(new TenantIdentifier(null, null, "t2"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), tenantConfig1)}; @@ -237,19 +239,21 @@ public void storageInstanceIsReusedAcrossTenantsComplex() StorageLayer.loadAllTenantStorage(process.getProcess(), tenants); - assertSame(StorageLayer.getStorage("abc", null, process.getProcess()), - StorageLayer.getStorage(null, null, process.getProcess())); + assertSame(StorageLayer.getStorage(new TenantIdentifier("abc", null, null), process.getProcess()), + StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess())); - assertSame(StorageLayer.getStorage("abc", "t1", process.getProcess()), - StorageLayer.getStorage(null, "t2", process.getProcess())); + assertSame(StorageLayer.getStorage(new TenantIdentifier("abc", null, "t1"), process.getProcess()), + StorageLayer.getStorage(new TenantIdentifier(null, null, "t2"), process.getProcess())); - assertNotSame(StorageLayer.getStorage("abc", "t1", process.getProcess()), - StorageLayer.getStorage(null, null, process.getProcess())); + assertNotSame(StorageLayer.getStorage(new TenantIdentifier("abc", null, "t1"), process.getProcess()), + StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess())); - Assert.assertEquals(Config.getConfig(null, null, process.getProcess()).getAccessTokenValidity(), + Assert.assertEquals( + Config.getConfig(new TenantIdentifier(null, null, null), process.getProcess()).getAccessTokenValidity(), (long) 3600 * 1000); - Assert.assertEquals(Config.getConfig("abc", null, process.getProcess()).getAccessTokenValidity(), + Assert.assertEquals(Config.getConfig(new TenantIdentifier("abc", null, null), process.getProcess()) + .getAccessTokenValidity(), (long) 3601 * 1000); Assert.assertEquals( @@ -257,15 +261,17 @@ public void storageInstanceIsReusedAcrossTenantsComplex() .size(), 4); Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( - (Start) StorageLayer.getStorage("abc", "t1", process.getProcess())) + (Start) StorageLayer.getStorage(new TenantIdentifier("abc", null, "t1"), process.getProcess())) .getConnectionPoolSize(), 11); Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( - (Start) StorageLayer.getStorage("random", "t2", process.getProcess())) + (Start) StorageLayer.getStorage(new TenantIdentifier("random", null, "t2"), + process.getProcess())) .getConnectionPoolSize(), 11); Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( - (Start) StorageLayer.getStorage("random", null, process.getProcess())) + (Start) StorageLayer.getStorage(new TenantIdentifier("random", null, null), + process.getProcess())) .getConnectionPoolSize(), 10); process.kill(); @@ -288,7 +294,7 @@ public void mergingTenantWithBaseConfigWithInvalidConfigThrowsErrorWorks() try { TenantConfig[] tenants = new TenantConfig[]{ - new TenantConfig("abc", null, new EmailPasswordConfig(false), + new TenantConfig(new TenantIdentifier("abc", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), tenantConfig)}; @@ -321,7 +327,7 @@ public void mergingTenantWithBaseConfigWithConflictingConfigsThrowsError() try { TenantConfig[] tenants = new TenantConfig[]{ - new TenantConfig("abc", null, new EmailPasswordConfig(false), + new TenantConfig(new TenantIdentifier("abc", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), tenantConfig)}; @@ -355,7 +361,7 @@ public void mergingDifferentConnectionPoolIdTenantWithBaseConfigWithConflictingC try { TenantConfig[] tenants = new TenantConfig[]{ - new TenantConfig("abc", null, new EmailPasswordConfig(false), + new TenantConfig(new TenantIdentifier("abc", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), tenantConfig)}; @@ -374,7 +380,8 @@ public void mergingDifferentConnectionPoolIdTenantWithBaseConfigWithConflictingC @Test public void mergingDifferentUserPoolIdTenantWithBaseConfigWithConflictingConfigsShouldNotThrowsError() - throws InterruptedException, IOException, InvalidConfigException, DbInitException, TenantNotFoundException { + throws InterruptedException, IOException, InvalidConfigException, DbInitException, + TenantOrAppNotFoundException { String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); @@ -389,7 +396,7 @@ public void mergingDifferentUserPoolIdTenantWithBaseConfigWithConflictingConfigs tenantConfig.add("postgresql_table_schema", new JsonPrimitive("supertokens2")); TenantConfig[] tenants = new TenantConfig[]{ - new TenantConfig("abc", null, new EmailPasswordConfig(false), + new TenantConfig(new TenantIdentifier("abc", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), tenantConfig)}; @@ -398,23 +405,23 @@ public void mergingDifferentUserPoolIdTenantWithBaseConfigWithConflictingConfigs StorageLayer.loadAllTenantStorage(process.getProcess(), tenants); Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( - (Start) StorageLayer.getStorage("abc", null, process.getProcess())) + (Start) StorageLayer.getStorage(new TenantIdentifier("abc", null, null), process.getProcess())) .getTableSchema(), "supertokens2"); Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( - (Start) StorageLayer.getStorage("abc", null, process.getProcess())) + (Start) StorageLayer.getStorage(new TenantIdentifier("abc", null, null), process.getProcess())) .getConnectionPoolSize(), 11); Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( - (Start) StorageLayer.getStorage("abc", null, process.getProcess())) + (Start) StorageLayer.getStorage(new TenantIdentifier("abc", null, null), process.getProcess())) .getThirdPartyUsersTable(), "supertokens2.random"); Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( - (Start) StorageLayer.getStorage(null, null, process.getProcess())) + (Start) StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess())) .getTableSchema(), "public"); Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( - (Start) StorageLayer.getStorage(null, null, process.getProcess())) + (Start) StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess())) .getConnectionPoolSize(), 10); Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( - (Start) StorageLayer.getStorage(null, null, process.getProcess())) + (Start) StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess())) .getThirdPartyUsersTable(), "thirdparty_users"); process.kill(); @@ -423,7 +430,8 @@ public void mergingDifferentUserPoolIdTenantWithBaseConfigWithConflictingConfigs @Test public void newStorageIsNotCreatedWhenSameTenantIsAdded() - throws InterruptedException, IOException, InvalidConfigException, DbInitException, TenantNotFoundException { + throws InterruptedException, IOException, InvalidConfigException, DbInitException, + TenantOrAppNotFoundException { String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); @@ -432,13 +440,13 @@ public void newStorageIsNotCreatedWhenSameTenantIsAdded() process.startProcess(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - Storage existingStorage = StorageLayer.getStorage(null, null, process.getProcess()); + Storage existingStorage = StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess()); JsonObject tenantConfig = new JsonObject(); tenantConfig.add("access_token_validity", new JsonPrimitive(3601)); TenantConfig[] tenants = new TenantConfig[]{ - new TenantConfig("abc", null, new EmailPasswordConfig(false), + new TenantConfig(new TenantIdentifier("abc", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), tenantConfig)}; @@ -447,12 +455,15 @@ public void newStorageIsNotCreatedWhenSameTenantIsAdded() StorageLayer.loadAllTenantStorage(process.getProcess(), tenants); - assertSame(StorageLayer.getStorage("abc", null, process.getProcess()), existingStorage); + assertSame(StorageLayer.getStorage(new TenantIdentifier("abc", null, null), process.getProcess()), + existingStorage); - Assert.assertEquals(Config.getConfig(null, null, process.getProcess()).getAccessTokenValidity(), + Assert.assertEquals( + Config.getConfig(new TenantIdentifier(null, null, null), process.getProcess()).getAccessTokenValidity(), (long) 3600 * 1000); - Assert.assertEquals(Config.getConfig("abc", null, process.getProcess()).getAccessTokenValidity(), + Assert.assertEquals(Config.getConfig(new TenantIdentifier("abc", null, null), process.getProcess()) + .getAccessTokenValidity(), (long) 3601 * 1000); Assert.assertEquals( @@ -465,7 +476,8 @@ public void newStorageIsNotCreatedWhenSameTenantIsAdded() @Test public void testDifferentWaysToGetConfigBasedOnConnectionURIAndTenantId() - throws InterruptedException, IOException, InvalidConfigException, DbInitException, TenantNotFoundException { + throws InterruptedException, IOException, InvalidConfigException, DbInitException, + TenantOrAppNotFoundException { String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); @@ -479,7 +491,7 @@ public void testDifferentWaysToGetConfigBasedOnConnectionURIAndTenantId() { JsonObject tenantConfig = new JsonObject(); tenantConfig.add("postgresql_connection_pool_size", new JsonPrimitive(12)); - tenants[0] = new TenantConfig("c1", null, new EmailPasswordConfig(false), + tenants[0] = new TenantConfig(new TenantIdentifier("c1", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), tenantConfig); @@ -488,7 +500,7 @@ public void testDifferentWaysToGetConfigBasedOnConnectionURIAndTenantId() { JsonObject tenantConfig = new JsonObject(); tenantConfig.add("postgresql_connection_pool_size", new JsonPrimitive(13)); - tenants[1] = new TenantConfig("c1", "t1", new EmailPasswordConfig(false), + tenants[1] = new TenantConfig(new TenantIdentifier("c1", null, "t1"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), tenantConfig); @@ -497,7 +509,7 @@ public void testDifferentWaysToGetConfigBasedOnConnectionURIAndTenantId() { JsonObject tenantConfig = new JsonObject(); tenantConfig.add("postgresql_connection_pool_size", new JsonPrimitive(14)); - tenants[2] = new TenantConfig(null, "t2", new EmailPasswordConfig(false), + tenants[2] = new TenantConfig(new TenantIdentifier(null, null, "t2"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), tenantConfig); @@ -506,7 +518,7 @@ public void testDifferentWaysToGetConfigBasedOnConnectionURIAndTenantId() { JsonObject tenantConfig = new JsonObject(); tenantConfig.add("postgresql_connection_pool_size", new JsonPrimitive(15)); - tenants[3] = new TenantConfig(null, "t1", new EmailPasswordConfig(false), + tenants[3] = new TenantConfig(new TenantIdentifier(null, null, "t1"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), tenantConfig); @@ -517,35 +529,35 @@ public void testDifferentWaysToGetConfigBasedOnConnectionURIAndTenantId() StorageLayer.loadAllTenantStorage(process.getProcess(), tenants); Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( - (Start) StorageLayer.getStorage(null, null, process.getProcess())) + (Start) StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess())) .getConnectionPoolSize(), 10); Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( - (Start) StorageLayer.getStorage("c1", null, process.getProcess())) + (Start) StorageLayer.getStorage(new TenantIdentifier("c1", null, null), process.getProcess())) .getConnectionPoolSize(), 12); Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( - (Start) StorageLayer.getStorage("c1", "t1", process.getProcess())) + (Start) StorageLayer.getStorage(new TenantIdentifier("c1", null, "t1"), process.getProcess())) .getConnectionPoolSize(), 13); Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( - (Start) StorageLayer.getStorage(null, "t1", process.getProcess())) + (Start) StorageLayer.getStorage(new TenantIdentifier(null, null, "t1"), process.getProcess())) .getConnectionPoolSize(), 15); Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( - (Start) StorageLayer.getStorage("c2", null, process.getProcess())) + (Start) StorageLayer.getStorage(new TenantIdentifier("c2", null, null), process.getProcess())) .getConnectionPoolSize(), 10); Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( - (Start) StorageLayer.getStorage("c1", "t1", process.getProcess())) + (Start) StorageLayer.getStorage(new TenantIdentifier("c1", null, "t1"), process.getProcess())) .getConnectionPoolSize(), 13); Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( - (Start) StorageLayer.getStorage("c3", "t2", process.getProcess())) + (Start) StorageLayer.getStorage(new TenantIdentifier("c3", null, "t2"), process.getProcess())) .getConnectionPoolSize(), 14); Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( - (Start) StorageLayer.getStorage(null, "t2", process.getProcess())) + (Start) StorageLayer.getStorage(new TenantIdentifier(null, null, "t2"), process.getProcess())) .getConnectionPoolSize(), 14); process.kill(); @@ -569,7 +581,7 @@ public void differentUserPoolCreatedBasedOnConnectionUri() try { TenantConfig[] tenants = new TenantConfig[]{ - new TenantConfig("abc", null, new EmailPasswordConfig(false), + new TenantConfig(new TenantIdentifier("abc", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), tenantConfig)}; @@ -588,7 +600,8 @@ public void differentUserPoolCreatedBasedOnConnectionUri() @Test public void differentUserPoolCreatedBasedOnSchemaInConnectionUri() - throws InterruptedException, IOException, InvalidConfigException, DbInitException, TenantNotFoundException { + throws InterruptedException, IOException, InvalidConfigException, DbInitException, + TenantOrAppNotFoundException { String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); @@ -602,7 +615,7 @@ public void differentUserPoolCreatedBasedOnSchemaInConnectionUri() new JsonPrimitive("postgresql://root:root@localhost:5432/supertokens?currentSchema=random")); TenantConfig[] tenants = new TenantConfig[]{ - new TenantConfig("abc", null, new EmailPasswordConfig(false), + new TenantConfig(new TenantIdentifier("abc", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), tenantConfig)}; @@ -611,15 +624,15 @@ public void differentUserPoolCreatedBasedOnSchemaInConnectionUri() StorageLayer.loadAllTenantStorage(process.getProcess(), tenants); Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( - (Start) StorageLayer.getStorage("abc", null, process.getProcess())) + (Start) StorageLayer.getStorage(new TenantIdentifier("abc", null, null), process.getProcess())) .getTableSchema(), "random"); Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( - (Start) StorageLayer.getStorage(null, null, process.getProcess())) + (Start) StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess())) .getTableSchema(), "public"); - assertNotSame(StorageLayer.getStorage("abc", null, process.getProcess()), - StorageLayer.getStorage(null, null, process.getProcess())); + assertNotSame(StorageLayer.getStorage(new TenantIdentifier("abc", null, null), process.getProcess()), + StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess())); Assert.assertEquals( process.getProcess().getResourceDistributor().getAllResourcesWithResourceKey(StorageLayer.RESOURCE_KEY) @@ -631,7 +644,8 @@ public void differentUserPoolCreatedBasedOnSchemaInConnectionUri() @Test public void multipleTenantsSameUserPoolAndConnectionPoolShouldWork() - throws InterruptedException, IOException, InvalidConfigException, DbInitException, TenantNotFoundException { + throws InterruptedException, IOException, InvalidConfigException, DbInitException, + TenantOrAppNotFoundException { String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); @@ -643,7 +657,7 @@ public void multipleTenantsSameUserPoolAndConnectionPoolShouldWork() JsonObject tenantConfig = new JsonObject(); TenantConfig[] tenants = new TenantConfig[]{ - new TenantConfig("abc", null, new EmailPasswordConfig(false), + new TenantConfig(new TenantIdentifier("abc", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), tenantConfig)}; @@ -651,8 +665,8 @@ public void multipleTenantsSameUserPoolAndConnectionPoolShouldWork() StorageLayer.loadAllTenantStorage(process.getProcess(), tenants); - assertSame(StorageLayer.getStorage("abc", null, process.getProcess()), - StorageLayer.getStorage(null, null, process.getProcess())); + assertSame(StorageLayer.getStorage(new TenantIdentifier("abc", null, null), process.getProcess()), + StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess())); Assert.assertEquals( process.getProcess().getResourceDistributor().getAllResourcesWithResourceKey(StorageLayer.RESOURCE_KEY) @@ -664,7 +678,8 @@ public void multipleTenantsSameUserPoolAndConnectionPoolShouldWork() @Test public void multipleTenantsSameUserPoolAndDifferentConnectionPoolShouldWork() - throws InterruptedException, IOException, InvalidConfigException, DbInitException, TenantNotFoundException { + throws InterruptedException, IOException, InvalidConfigException, DbInitException, + TenantOrAppNotFoundException { String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); @@ -677,7 +692,7 @@ public void multipleTenantsSameUserPoolAndDifferentConnectionPoolShouldWork() tenantConfig.add("postgresql_connection_pool_size", new JsonPrimitive(20)); TenantConfig[] tenants = new TenantConfig[]{ - new TenantConfig("abc", null, new EmailPasswordConfig(false), + new TenantConfig(new TenantIdentifier("abc", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), tenantConfig)}; @@ -685,8 +700,8 @@ public void multipleTenantsSameUserPoolAndDifferentConnectionPoolShouldWork() StorageLayer.loadAllTenantStorage(process.getProcess(), tenants); - assertNotSame(StorageLayer.getStorage("abc", null, process.getProcess()), - StorageLayer.getStorage(null, null, process.getProcess())); + assertNotSame(StorageLayer.getStorage(new TenantIdentifier("abc", null, null), process.getProcess()), + StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess())); Assert.assertEquals( process.getProcess().getResourceDistributor().getAllResourcesWithResourceKey(StorageLayer.RESOURCE_KEY) From 9cbc85f23b64dff873bb2501d578ab6c4b843f58 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 7 Feb 2023 11:50:13 +0530 Subject: [PATCH 019/106] adds more placeholder functions --- .../io/supertokens/storage/postgresql/Start.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index a662f96c..5e86c82f 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -46,6 +46,7 @@ import io.supertokens.pluginInterface.jwt.sqlstorage.JWTRecipeSQLStorage; import io.supertokens.pluginInterface.multitenancy.MultitenancyStorage; import io.supertokens.pluginInterface.multitenancy.TenantConfig; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateTenantException; import io.supertokens.pluginInterface.multitenancy.exceptions.UnknownTenantException; import io.supertokens.pluginInterface.passwordless.PasswordlessCode; @@ -1870,12 +1871,22 @@ public void overwriteTenantConfig(TenantConfig config) throws UnknownTenantExcep } @Override - public void deleteTenant(String tenantId) throws UnknownTenantException { + public void deleteTenant(TenantIdentifier tenantIdentifier) throws UnknownTenantException { // TODO: } @Override - public TenantConfig getTenantConfigForTenantId(String tenantId) { + public void deleteApp(TenantIdentifier tenantIdentifier) throws UnknownTenantException { + // TODO: + } + + @Override + public void deleteConnectionUriDomainMapping(TenantIdentifier tenantIdentifier) throws UnknownTenantException { + // TODO: + } + + @Override + public TenantConfig getTenantConfigForTenantIdentifier(TenantIdentifier tenantIdentifier) { // TODO: return null; } From ea05ab316c903b607c8d61ce939c7e427e748243 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 7 Feb 2023 12:41:21 +0530 Subject: [PATCH 020/106] removes use of quiteprogramexception --- .../io/supertokens/storage/postgresql/ConnectionPool.java | 3 +-- .../io/supertokens/storage/postgresql/config/Config.java | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/ConnectionPool.java b/src/main/java/io/supertokens/storage/postgresql/ConnectionPool.java index 654f0cab..0391d503 100644 --- a/src/main/java/io/supertokens/storage/postgresql/ConnectionPool.java +++ b/src/main/java/io/supertokens/storage/postgresql/ConnectionPool.java @@ -20,7 +20,6 @@ import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import io.supertokens.pluginInterface.exceptions.DbInitException; -import io.supertokens.pluginInterface.exceptions.QuitProgramFromPluginException; import io.supertokens.storage.postgresql.config.Config; import io.supertokens.storage.postgresql.config.PostgreSQLConfig; import io.supertokens.storage.postgresql.output.Logging; @@ -170,7 +169,7 @@ static void initPool(Start start) throws DbInitException { public static Connection getConnection(Start start) throws SQLException { if (getInstance(start) == null) { - throw new QuitProgramFromPluginException("Please call initPool before getConnection"); + throw new IllegalStateException("Please call initPool before getConnection"); } if (!start.enabled) { throw new SQLException("Storage layer disabled"); diff --git a/src/main/java/io/supertokens/storage/postgresql/config/Config.java b/src/main/java/io/supertokens/storage/postgresql/config/Config.java index c6f456b0..ac8bbdd8 100644 --- a/src/main/java/io/supertokens/storage/postgresql/config/Config.java +++ b/src/main/java/io/supertokens/storage/postgresql/config/Config.java @@ -22,7 +22,6 @@ import com.google.gson.JsonObject; import io.supertokens.pluginInterface.LOG_LEVEL; import io.supertokens.pluginInterface.exceptions.InvalidConfigException; -import io.supertokens.pluginInterface.exceptions.QuitProgramFromPluginException; import io.supertokens.storage.postgresql.ResourceDistributor; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.output.Logging; @@ -44,7 +43,7 @@ private Config(Start start, JsonObject configJson, Set logLevels) thr try { config = loadPostgreSQLConfig(configJson); } catch (IOException e) { - throw new QuitProgramFromPluginException(e); + throw new RuntimeException(e); } } @@ -89,7 +88,7 @@ public static void assertThatConfigFromSameUserPoolIsNotConflicting(Start start, public static PostgreSQLConfig getConfig(Start start) { if (getInstance(start) == null) { - throw new QuitProgramFromPluginException("Please call loadConfig() before calling getConfig()"); + throw new IllegalStateException("Please call loadConfig() before calling getConfig()"); } return getInstance(start).config; } From 18fd7aec928fd65e79008def8f419af70703c066 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Wed, 8 Feb 2023 17:35:02 +0530 Subject: [PATCH 021/106] small change --- .../io/supertokens/storage/postgresql/Start.java | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 5e86c82f..8d343ab5 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -1885,21 +1885,9 @@ public void deleteConnectionUriDomainMapping(TenantIdentifier tenantIdentifier) // TODO: } - @Override - public TenantConfig getTenantConfigForTenantIdentifier(TenantIdentifier tenantIdentifier) { - // TODO: - return null; - } - @Override public TenantConfig[] getAllTenants() { // TODO: return new TenantConfig[0]; } - - @Override - public TenantConfig[] getAllTenantsWithThirdPartyId(String thirdPartyId) { - // TODO: - return new TenantConfig[0]; - } } From b61ddc760718b045d13c962e59f91d0a4bd7119a Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Wed, 8 Feb 2023 18:49:12 +0530 Subject: [PATCH 022/106] adds new function skeleton --- .../io/supertokens/storage/postgresql/Start.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 8d343ab5..47ca7d13 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -1890,4 +1890,16 @@ public TenantConfig[] getAllTenants() { // TODO: return new TenantConfig[0]; } + + @Override + public void addUserIdToTenant(TenantIdentifier tenantIdentifier, String userId) + throws UnknownTenantException, UnknownUserIdException { + // TODO: + } + + @Override + public void addRoleToTenant(TenantIdentifier tenantIdentifier, String role) + throws UnknownTenantException, UnknownRoleException { + // TODO: + } } From 06a06433d067bde4c772813eaee7112f501c3f41 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Thu, 9 Feb 2023 12:04:05 +0530 Subject: [PATCH 023/106] adds more skeleton functions --- .../supertokens/storage/postgresql/Start.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 47ca7d13..0ce95f4f 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -1865,6 +1865,16 @@ public void createTenant(TenantConfig config) throws DuplicateTenantException { // TODO: } + @Override + public void addTenantIdInUserPool(TenantIdentifier tenantIdentifier) throws DuplicateTenantException { + // TODO: + } + + @Override + public void deleteTenantIdInUserPool(TenantIdentifier tenantIdentifier) throws UnknownTenantException { + // TODO: + } + @Override public void overwriteTenantConfig(TenantConfig config) throws UnknownTenantException { // TODO: @@ -1902,4 +1912,14 @@ public void addRoleToTenant(TenantIdentifier tenantIdentifier, String role) throws UnknownTenantException, UnknownRoleException { // TODO: } + + @Override + public void markAppIdAsDeleted(String appId) throws UnknownTenantException { + // TODO: + } + + @Override + public void markConnectionUriDomainAsDeleted(String connectionUriDomain) throws UnknownTenantException { + // TODO: + } } From 0c0931f51932ebd2fd1993eef9912f67e3b4fcee Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Thu, 9 Feb 2023 20:09:26 +0530 Subject: [PATCH 024/106] updates exception import --- .../supertokens/storage/postgresql/Start.java | 21 ++++++++++--------- .../test/multitenancy/StorageLayerTest.java | 2 +- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 0ce95f4f..ee0a946a 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -48,7 +48,7 @@ import io.supertokens.pluginInterface.multitenancy.TenantConfig; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateTenantException; -import io.supertokens.pluginInterface.multitenancy.exceptions.UnknownTenantException; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.passwordless.PasswordlessCode; import io.supertokens.pluginInterface.passwordless.PasswordlessDevice; import io.supertokens.pluginInterface.passwordless.exception.*; @@ -1871,27 +1871,28 @@ public void addTenantIdInUserPool(TenantIdentifier tenantIdentifier) throws Dupl } @Override - public void deleteTenantIdInUserPool(TenantIdentifier tenantIdentifier) throws UnknownTenantException { + public void deleteTenantIdInUserPool(TenantIdentifier tenantIdentifier) throws TenantOrAppNotFoundException { // TODO: } @Override - public void overwriteTenantConfig(TenantConfig config) throws UnknownTenantException { + public void overwriteTenantConfig(TenantConfig config) throws TenantOrAppNotFoundException { // TODO: } @Override - public void deleteTenant(TenantIdentifier tenantIdentifier) throws UnknownTenantException { + public void deleteTenant(TenantIdentifier tenantIdentifier) throws TenantOrAppNotFoundException { // TODO: } @Override - public void deleteApp(TenantIdentifier tenantIdentifier) throws UnknownTenantException { + public void deleteApp(TenantIdentifier tenantIdentifier) throws TenantOrAppNotFoundException { // TODO: } @Override - public void deleteConnectionUriDomainMapping(TenantIdentifier tenantIdentifier) throws UnknownTenantException { + public void deleteConnectionUriDomainMapping(TenantIdentifier tenantIdentifier) throws + TenantOrAppNotFoundException { // TODO: } @@ -1903,23 +1904,23 @@ public TenantConfig[] getAllTenants() { @Override public void addUserIdToTenant(TenantIdentifier tenantIdentifier, String userId) - throws UnknownTenantException, UnknownUserIdException { + throws TenantOrAppNotFoundException, UnknownUserIdException { // TODO: } @Override public void addRoleToTenant(TenantIdentifier tenantIdentifier, String role) - throws UnknownTenantException, UnknownRoleException { + throws TenantOrAppNotFoundException, UnknownRoleException { // TODO: } @Override - public void markAppIdAsDeleted(String appId) throws UnknownTenantException { + public void markAppIdAsDeleted(String appId) throws TenantOrAppNotFoundException { // TODO: } @Override - public void markConnectionUriDomainAsDeleted(String connectionUriDomain) throws UnknownTenantException { + public void markConnectionUriDomainAsDeleted(String connectionUriDomain) throws TenantOrAppNotFoundException { // TODO: } } diff --git a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java index c824737a..552d17c4 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java @@ -20,7 +20,7 @@ import com.google.gson.JsonPrimitive; import io.supertokens.ProcessState; import io.supertokens.config.Config; -import io.supertokens.exceptions.TenantOrAppNotFoundException; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.featureflag.EE_FEATURES; import io.supertokens.featureflag.FeatureFlagTestContent; import io.supertokens.pluginInterface.Storage; From 489ec419fc2ee2d203c9cd6bc9e94b5b59cdfd44 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Mon, 13 Feb 2023 17:42:41 +0530 Subject: [PATCH 025/106] adds skeleton for tenantIdentifier for emailpassword and useridmapping recipes --- .../supertokens/storage/postgresql/Start.java | 90 +++++++++--------- .../queries/EmailPasswordQueries.java | 69 ++------------ .../postgresql/test/ExceptionParsingTest.java | 91 ++++++++++--------- 3 files changed, 97 insertions(+), 153 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index ee0a946a..520e361d 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -662,8 +662,9 @@ public void modifyConfigToAddANewUserPoolForTesting(JsonObject config, int poolN } @Override - public void signUp(UserInfo userInfo) + public void signUp(TenantIdentifier tenantIdentifier, UserInfo userInfo) throws StorageQueryException, DuplicateUserIdException, DuplicateEmailException { + // TODO.. try { EmailPasswordQueries.signUp(this, userInfo.id, userInfo.email, userInfo.passwordHash, userInfo.timeJoined); } catch (StorageTransactionLogicException eTemp) { @@ -694,7 +695,8 @@ public void signUp(UserInfo userInfo) } @Override - public void deleteEmailPasswordUser(String userId) throws StorageQueryException { + public void deleteEmailPasswordUser(TenantIdentifier tenantIdentifier, String userId) throws StorageQueryException { + // TODO.. try { EmailPasswordQueries.deleteUser(this, userId); } catch (StorageTransactionLogicException e) { @@ -703,7 +705,8 @@ public void deleteEmailPasswordUser(String userId) throws StorageQueryException } @Override - public UserInfo getUserInfoUsingId(String id) throws StorageQueryException { + public UserInfo getUserInfoUsingId(TenantIdentifier tenantIdentifier, String id) throws StorageQueryException { + // TODO.. try { return EmailPasswordQueries.getUserInfoUsingId(this, id); } catch (SQLException e) { @@ -712,7 +715,9 @@ public UserInfo getUserInfoUsingId(String id) throws StorageQueryException { } @Override - public UserInfo getUserInfoUsingEmail(String email) throws StorageQueryException { + public UserInfo getUserInfoUsingEmail(TenantIdentifier tenantIdentifier, String email) + throws StorageQueryException { + // TODO.. try { return EmailPasswordQueries.getUserInfoUsingEmail(this, email); } catch (SQLException e) { @@ -721,8 +726,9 @@ public UserInfo getUserInfoUsingEmail(String email) throws StorageQueryException } @Override - public void addPasswordResetToken(PasswordResetTokenInfo passwordResetTokenInfo) + public void addPasswordResetToken(TenantIdentifier tenantIdentifier, PasswordResetTokenInfo passwordResetTokenInfo) throws StorageQueryException, UnknownUserIdException, DuplicatePasswordResetTokenException { + // TODO.. try { EmailPasswordQueries.addPasswordResetToken(this, passwordResetTokenInfo.userId, passwordResetTokenInfo.token, passwordResetTokenInfo.tokenExpiry); @@ -751,7 +757,9 @@ public void addPasswordResetToken(PasswordResetTokenInfo passwordResetTokenInfo) } @Override - public PasswordResetTokenInfo getPasswordResetTokenInfo(String token) throws StorageQueryException { + public PasswordResetTokenInfo getPasswordResetTokenInfo(TenantIdentifier tenantIdentifier, String token) + throws StorageQueryException { + // TODO.. try { return EmailPasswordQueries.getPasswordResetTokenInfo(this, token); } catch (SQLException e) { @@ -760,7 +768,9 @@ public PasswordResetTokenInfo getPasswordResetTokenInfo(String token) throws Sto } @Override - public PasswordResetTokenInfo[] getAllPasswordResetTokenInfoForUser(String userId) throws StorageQueryException { + public PasswordResetTokenInfo[] getAllPasswordResetTokenInfoForUser(TenantIdentifier tenantIdentifier, + String userId) throws StorageQueryException { + // TODO.. try { return EmailPasswordQueries.getAllPasswordResetTokenInfoForUser(this, userId); } catch (SQLException e) { @@ -970,37 +980,6 @@ public boolean isEmailVerified(String userId, String email) throws StorageQueryE } } - @Override - @Deprecated - public UserInfo[] getUsers(@Nonnull String userId, @Nonnull Long timeJoined, @Nonnull Integer limit, - @Nonnull String timeJoinedOrder) throws StorageQueryException { - try { - return EmailPasswordQueries.getUsersInfo(this, userId, timeJoined, limit, timeJoinedOrder); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - - @Override - @Deprecated - public UserInfo[] getUsers(@Nonnull Integer limit, @Nonnull String timeJoinedOrder) throws StorageQueryException { - try { - return EmailPasswordQueries.getUsersInfo(this, limit, timeJoinedOrder); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - - @Override - @Deprecated - public long getUsersCount() throws StorageQueryException { - try { - return EmailPasswordQueries.getUsersCount(this); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - @Override public void deleteExpiredPasswordResetTokens() throws StorageQueryException { try { @@ -1142,7 +1121,9 @@ public io.supertokens.pluginInterface.thirdparty.UserInfo[] getThirdPartyUsersBy } @Override - public long getUsersCount(RECIPE_ID[] includeRecipeIds) throws StorageQueryException { + public long getUsersCount(TenantIdentifier tenantIdentifier, RECIPE_ID[] includeRecipeIds) + throws StorageQueryException { + // TODO.. try { return GeneralQueries.getUsersCount(this, includeRecipeIds); } catch (SQLException e) { @@ -1151,10 +1132,12 @@ public long getUsersCount(RECIPE_ID[] includeRecipeIds) throws StorageQueryExcep } @Override - public AuthRecipeUserInfo[] getUsers(@NotNull Integer limit, @NotNull String timeJoinedOrder, + public AuthRecipeUserInfo[] getUsers(TenantIdentifier tenantIdentifier, @NotNull Integer limit, + @NotNull String timeJoinedOrder, @Nullable RECIPE_ID[] includeRecipeIds, @Nullable String userId, @Nullable Long timeJoined) throws StorageQueryException { + // TODO.. try { return GeneralQueries.getUsers(this, limit, timeJoinedOrder, includeRecipeIds, userId, timeJoined); } catch (SQLException e) { @@ -1163,7 +1146,8 @@ public AuthRecipeUserInfo[] getUsers(@NotNull Integer limit, @NotNull String tim } @Override - public boolean doesUserIdExist(String userId) throws StorageQueryException { + public boolean doesUserIdExist(TenantIdentifier tenantIdentifier, String userId) throws StorageQueryException { + // TODO.. try { return GeneralQueries.doesUserIdExist(this, userId); } catch (SQLException e) { @@ -1765,9 +1749,10 @@ public boolean doesRoleExist_Transaction(TransactionConnection con, String role) } @Override - public void createUserIdMapping(String superTokensUserId, String externalUserId, + public void createUserIdMapping(TenantIdentifier tenantIdentifier, String superTokensUserId, String externalUserId, @Nullable String externalUserIdInfo) throws StorageQueryException, UnknownSuperTokensUserIdException, UserIdMappingAlreadyExistsException { + // TODO.. try { UserIdMappingQueries.createUserIdMapping(this, superTokensUserId, externalUserId, externalUserIdInfo); } catch (SQLException e) { @@ -1798,7 +1783,9 @@ public void createUserIdMapping(String superTokensUserId, String externalUserId, } @Override - public boolean deleteUserIdMapping(String userId, boolean isSuperTokensUserId) throws StorageQueryException { + public boolean deleteUserIdMapping(TenantIdentifier tenantIdentifier, String userId, boolean isSuperTokensUserId) + throws StorageQueryException { + // TODO.. try { if (isSuperTokensUserId) { return UserIdMappingQueries.deleteUserIdMappingWithSuperTokensUserId(this, userId); @@ -1811,8 +1798,9 @@ public boolean deleteUserIdMapping(String userId, boolean isSuperTokensUserId) t } @Override - public UserIdMapping getUserIdMapping(String userId, boolean isSuperTokensUserId) throws StorageQueryException { - + public UserIdMapping getUserIdMapping(TenantIdentifier tenantIdentifier, String userId, boolean isSuperTokensUserId) + throws StorageQueryException { + // TODO.. try { if (isSuperTokensUserId) { return UserIdMappingQueries.getuseraIdMappingWithSuperTokensUserId(this, userId); @@ -1825,7 +1813,9 @@ public UserIdMapping getUserIdMapping(String userId, boolean isSuperTokensUserId } @Override - public UserIdMapping[] getUserIdMapping(String userId) throws StorageQueryException { + public UserIdMapping[] getUserIdMapping(TenantIdentifier tenantIdentifier, String userId) + throws StorageQueryException { + // TODO.. try { return UserIdMappingQueries.getUserIdMappingWithEitherSuperTokensUserIdOrExternalUserId(this, userId); } catch (SQLException e) { @@ -1834,9 +1824,11 @@ public UserIdMapping[] getUserIdMapping(String userId) throws StorageQueryExcept } @Override - public boolean updateOrDeleteExternalUserIdInfo(String userId, boolean isSuperTokensUserId, + public boolean updateOrDeleteExternalUserIdInfo(TenantIdentifier tenantIdentifier, String userId, + boolean isSuperTokensUserId, @Nullable String externalUserIdInfo) throws StorageQueryException { + // TODO.. try { if (isSuperTokensUserId) { return UserIdMappingQueries.updateOrDeleteExternalUserIdInfoWithSuperTokensUserId(this, userId, @@ -1851,8 +1843,10 @@ public boolean updateOrDeleteExternalUserIdInfo(String userId, boolean isSuperTo } @Override - public HashMap getUserIdMappingForSuperTokensIds(ArrayList userIds) + public HashMap getUserIdMappingForSuperTokensIds(TenantIdentifier tenantIdentifier, + ArrayList userIds) throws StorageQueryException { + // TODO.. try { return UserIdMappingQueries.getUserIdMappingWithUserIds(this, userIds); } catch (SQLException e) { diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java index 20a44705..0f1479f4 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java @@ -33,7 +33,6 @@ import java.util.List; import static io.supertokens.pluginInterface.RECIPE_ID.EMAIL_PASSWORD; -import static io.supertokens.storage.postgresql.PreparedStatementValueSetter.NO_OP_SETTER; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.update; import static io.supertokens.storage.postgresql.config.Config.getConfig; @@ -61,10 +60,13 @@ static String getQueryToCreatePasswordResetTokensTable(Start start) { // @formatter:off return "CREATE TABLE IF NOT EXISTS " + passwordResetTokensTable + " (" + "user_id CHAR(36) NOT NULL," - + "token VARCHAR(128) NOT NULL CONSTRAINT " + Utils.getConstraintName(schema, passwordResetTokensTable, "token", "key") + " UNIQUE," + + "token VARCHAR(128) NOT NULL CONSTRAINT " + + Utils.getConstraintName(schema, passwordResetTokensTable, "token", "key") + " UNIQUE," + "token_expiry BIGINT NOT NULL," - + "CONSTRAINT " + Utils.getConstraintName(schema, passwordResetTokensTable, null, "pkey") + " PRIMARY KEY (user_id, token)," - + ("CONSTRAINT " + Utils.getConstraintName(schema, passwordResetTokensTable, "user_id", "fkey") + " FOREIGN KEY (user_id)" + + "CONSTRAINT " + Utils.getConstraintName(schema, passwordResetTokensTable, null, "pkey") + + " PRIMARY KEY (user_id, token)," + + ("CONSTRAINT " + Utils.getConstraintName(schema, passwordResetTokensTable, "user_id", "fkey") + + " FOREIGN KEY (user_id)" + " REFERENCES " + Config.getConfig(start).getEmailPasswordUsersTable() + "(user_id)" + " ON DELETE CASCADE ON UPDATE CASCADE);"); // @formatter:on @@ -128,7 +130,8 @@ public static PasswordResetTokenInfo[] getAllPasswordResetTokenInfoForUser(Start } public static PasswordResetTokenInfo[] getAllPasswordResetTokenInfoForUser_Transaction(Start start, Connection con, - String userId) throws SQLException, StorageQueryException { + String userId) + throws SQLException, StorageQueryException { String QUERY = "SELECT user_id, token, token_expiry FROM " + getConfig(start).getPasswordResetTokensTable() + " WHERE user_id = ? FOR UPDATE"; @@ -158,62 +161,6 @@ public static UserInfo getUserInfoUsingId_Transaction(Start start, Connection co }); } - @Deprecated - public static UserInfo[] getUsersInfo(Start start, Integer limit, String timeJoinedOrder) - throws SQLException, StorageQueryException { - String QUERY = "SELECT user_id, email, password_hash, time_joined FROM " - + getConfig(start).getEmailPasswordUsersTable() + " ORDER BY time_joined " + timeJoinedOrder - + ", user_id DESC LIMIT ?"; - return execute(start, QUERY, pst -> pst.setInt(1, limit), result -> { - List temp = new ArrayList<>(); - while (result.next()) { - temp.add(UserInfoRowMapper.getInstance().mapOrThrow(result)); - } - UserInfo[] finalResult = new UserInfo[temp.size()]; - for (int i = 0; i < temp.size(); i++) { - finalResult[i] = temp.get(i); - } - return finalResult; - }); - } - - @Deprecated - public static UserInfo[] getUsersInfo(Start start, String userId, Long timeJoined, Integer limit, - String timeJoinedOrder) throws SQLException, StorageQueryException { - String timeJoinedOrderSymbol = timeJoinedOrder.equals("ASC") ? ">" : "<"; - String QUERY = "SELECT user_id, email, password_hash, time_joined FROM " - + getConfig(start).getEmailPasswordUsersTable() + " WHERE time_joined " + timeJoinedOrderSymbol - + " ? OR (time_joined = ? AND user_id <= ?) ORDER BY time_joined " + timeJoinedOrder - + ", user_id DESC LIMIT ?"; - return execute(start, QUERY, pst -> { - pst.setLong(1, timeJoined); - pst.setLong(2, timeJoined); - pst.setString(3, userId); - pst.setInt(4, limit); - }, result -> { - List temp = new ArrayList<>(); - while (result.next()) { - temp.add(UserInfoRowMapper.getInstance().mapOrThrow(result)); - } - UserInfo[] finalResult = new UserInfo[temp.size()]; - for (int i = 0; i < temp.size(); i++) { - finalResult[i] = temp.get(i); - } - return finalResult; - }); - } - - @Deprecated - public static long getUsersCount(Start start) throws SQLException, StorageQueryException { - String QUERY = "SELECT COUNT(*) as total FROM " + getConfig(start).getEmailPasswordUsersTable(); - return execute(start, QUERY, NO_OP_SETTER, result -> { - if (result.next()) { - return result.getLong("total"); - } - return 0L; - }); - } - public static PasswordResetTokenInfo getPasswordResetTokenInfo(Start start, String token) throws SQLException, StorageQueryException { String QUERY = "SELECT user_id, token, token_expiry FROM " + getConfig(start).getPasswordResetTokensTable() diff --git a/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java b/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java index eced732e..ce80e26c 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java @@ -17,26 +17,6 @@ package io.supertokens.storage.postgresql.test; -import static junit.framework.TestCase.assertEquals; -import static org.junit.Assert.assertNotNull; - -import java.io.UnsupportedEncodingException; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.SignatureException; -import java.security.spec.InvalidKeySpecException; - -import javax.crypto.BadPaddingException; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; - -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TestRule; - import io.supertokens.ProcessState; import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.emailpassword.PasswordResetTokenInfo; @@ -52,8 +32,27 @@ import io.supertokens.pluginInterface.jwt.JWTSymmetricSigningKeyInfo; import io.supertokens.pluginInterface.jwt.exceptions.DuplicateKeyIdException; import io.supertokens.pluginInterface.jwt.sqlstorage.JWTRecipeSQLStorage; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.thirdparty.exception.DuplicateThirdPartyUserException; import io.supertokens.storageLayer.StorageLayer; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import java.io.UnsupportedEncodingException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; +import java.security.spec.InvalidKeySpecException; + +import static junit.framework.TestCase.assertEquals; +import static org.junit.Assert.assertNotNull; public class ExceptionParsingTest { @Rule @@ -72,7 +71,7 @@ public void beforeEach() { @Test public void thirdPartySignupExceptions() throws Exception { { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -104,7 +103,8 @@ public void thirdPartySignupExceptions() throws Exception { // expected } - assertEquals(storage.getUsersCount(new RECIPE_ID[] { RECIPE_ID.THIRD_PARTY }), 1); + assertEquals(storage.getUsersCount(new TenantIdentifier(null, null, null), + new RECIPE_ID[]{RECIPE_ID.THIRD_PARTY}), 1); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -114,7 +114,7 @@ public void thirdPartySignupExceptions() throws Exception { @Test public void emailPasswordSignupExceptions() throws Exception { { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -126,9 +126,9 @@ public void emailPasswordSignupExceptions() throws Exception { String userEmail = "useremail@asdf.fdas"; var info = new UserInfo(userId, userEmail, pwHash, System.currentTimeMillis()); - storage.signUp(info); + storage.signUp(new TenantIdentifier(null, null, null), info); try { - storage.signUp(info); + storage.signUp(new TenantIdentifier(null, null, null), info); throw new Exception("This should throw"); } catch (DuplicateUserIdException ex) { // expected @@ -136,13 +136,14 @@ public void emailPasswordSignupExceptions() throws Exception { var info2 = new UserInfo(userId2, userEmail, pwHash, System.currentTimeMillis()); try { - storage.signUp(info2); + storage.signUp(new TenantIdentifier(null, null, null), info2); throw new Exception("This should throw"); } catch (DuplicateEmailException ex) { // expected } - assertEquals(storage.getUsersCount(new RECIPE_ID[] { RECIPE_ID.EMAIL_PASSWORD }), 1); + assertEquals(storage.getUsersCount(new TenantIdentifier(null, null, null), + new RECIPE_ID[]{RECIPE_ID.EMAIL_PASSWORD}), 1); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -156,7 +157,7 @@ public void updateUsersEmail_TransactionExceptions() UnsupportedEncodingException, InvalidKeySpecException, IllegalBlockSizeException, StorageTransactionLogicException, DuplicateUserIdException, DuplicateEmailException { { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -171,8 +172,8 @@ public void updateUsersEmail_TransactionExceptions() var info = new UserInfo(userId, userEmail, pwHash, System.currentTimeMillis()); var info2 = new UserInfo(userId2, userEmail2, pwHash, System.currentTimeMillis()); - storage.signUp(info); - storage.signUp(info2); + storage.signUp(new TenantIdentifier(null, null, null), info); + storage.signUp(new TenantIdentifier(null, null, null), info2); storage.startTransaction(conn -> { try { storage.updateUsersEmail_Transaction(conn, userId, userEmail2); @@ -192,7 +193,8 @@ public void updateUsersEmail_TransactionExceptions() return true; }); - assertEquals(storage.getUsersCount(new RECIPE_ID[] { RECIPE_ID.EMAIL_PASSWORD }), 2); + assertEquals(storage.getUsersCount(new TenantIdentifier(null, null, null), + new RECIPE_ID[]{RECIPE_ID.EMAIL_PASSWORD}), 2); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -206,7 +208,7 @@ public void updateIsEmailVerified_TransactionExceptions() UnsupportedEncodingException, InvalidKeySpecException, IllegalBlockSizeException, StorageTransactionLogicException, DuplicateUserIdException, DuplicateEmailException { { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -255,7 +257,7 @@ public void updateIsEmailVerified_TransactionExceptions() @Test public void addPasswordResetTokenExceptions() throws Exception { { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -269,13 +271,13 @@ public void addPasswordResetTokenExceptions() throws Exception { var userInfo = new UserInfo(userId, userEmail, pwHash, System.currentTimeMillis()); var info = new PasswordResetTokenInfo(userId, tokenHash, System.currentTimeMillis() + 10000); try { - storage.addPasswordResetToken(info); + storage.addPasswordResetToken(new TenantIdentifier(null, null, null), info); } catch (UnknownUserIdException ex) { - storage.signUp(userInfo); + storage.signUp(new TenantIdentifier(null, null, null), userInfo); } - storage.addPasswordResetToken(info); + storage.addPasswordResetToken(new TenantIdentifier(null, null, null), info); try { - storage.addPasswordResetToken(info); + storage.addPasswordResetToken(new TenantIdentifier(null, null, null), info); throw new Exception("This should throw"); } catch (DuplicatePasswordResetTokenException ex) { // expected @@ -289,7 +291,7 @@ public void addPasswordResetTokenExceptions() throws Exception { @Test public void addEmailVerificationTokenExceptions() throws Exception { { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -316,7 +318,7 @@ public void addEmailVerificationTokenExceptions() throws Exception { @Test public void verifyEmailExceptions() throws Exception { { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -328,9 +330,9 @@ public void verifyEmailExceptions() throws Exception { String userEmail = "useremail@asdf.fdas"; var info = new UserInfo(userId, userEmail, pwHash, System.currentTimeMillis()); - storage.signUp(info); + storage.signUp(new TenantIdentifier(null, null, null), info); try { - storage.signUp(info); + storage.signUp(new TenantIdentifier(null, null, null), info); throw new Exception("This should throw"); } catch (DuplicateUserIdException ex) { // expected @@ -338,13 +340,14 @@ public void verifyEmailExceptions() throws Exception { var info2 = new UserInfo(userId2, userEmail, pwHash, System.currentTimeMillis()); try { - storage.signUp(info2); + storage.signUp(new TenantIdentifier(null, null, null), info2); throw new Exception("This should throw"); } catch (DuplicateEmailException ex) { // expected } - assertEquals(storage.getUsersCount(new RECIPE_ID[] { RECIPE_ID.EMAIL_PASSWORD }), 1); + assertEquals(storage.getUsersCount(new TenantIdentifier(null, null, null), + new RECIPE_ID[]{RECIPE_ID.EMAIL_PASSWORD}), 1); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -357,7 +360,7 @@ public void setJWTSigningKey_TransactionExceptions() throws InterruptedException NoSuchPaddingException, BadPaddingException, UnsupportedEncodingException, InvalidKeySpecException, IllegalBlockSizeException, StorageTransactionLogicException { { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); From 8a988eaa754720e91935799864e22e489c2fc634 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 14 Feb 2023 17:24:28 +0530 Subject: [PATCH 026/106] changes to incorporate tenantIndetifier for key value storage --- .../supertokens/storage/postgresql/Start.java | 19 +++++--- .../storage/postgresql/test/DeadlockTest.java | 26 +++++------ .../test/multitenancy/StorageLayerTest.java | 44 ++++++++++--------- 3 files changed, 51 insertions(+), 38 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 520e361d..be178a74 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -469,7 +469,8 @@ public void deleteAllExpiredSessions() throws StorageQueryException { } @Override - public KeyValueInfo getKeyValue(String key) throws StorageQueryException { + public KeyValueInfo getKeyValue(TenantIdentifier tenantIdentifier, String key) throws StorageQueryException { + // TODO.. try { return GeneralQueries.getKeyValue(this, key); } catch (SQLException e) { @@ -478,7 +479,9 @@ public KeyValueInfo getKeyValue(String key) throws StorageQueryException { } @Override - public void setKeyValue(String key, KeyValueInfo info) throws StorageQueryException { + public void setKeyValue(TenantIdentifier tenantIdentifier, String key, KeyValueInfo info) + throws StorageQueryException { + // TODO.. try { GeneralQueries.setKeyValue(this, key, info); } catch (SQLException e) { @@ -533,8 +536,10 @@ public void updateSessionInfo_Transaction(TransactionConnection con, String sess } @Override - public void setKeyValue_Transaction(TransactionConnection con, String key, KeyValueInfo info) + public void setKeyValue_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, String key, + KeyValueInfo info) throws StorageQueryException { + // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { GeneralQueries.setKeyValue_Transaction(this, sqlCon, key, info); @@ -544,7 +549,9 @@ public void setKeyValue_Transaction(TransactionConnection con, String key, KeyVa } @Override - public KeyValueInfo getKeyValue_Transaction(TransactionConnection con, String key) throws StorageQueryException { + public KeyValueInfo getKeyValue_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, + String key) throws StorageQueryException { + // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { return GeneralQueries.getKeyValue_Transaction(this, sqlCon, key); @@ -579,7 +586,9 @@ public boolean canBeUsed(JsonObject configJson) { } @Override - public boolean isUserIdBeingUsedInNonAuthRecipe(String className, String userId) throws StorageQueryException { + public boolean isUserIdBeingUsedInNonAuthRecipe(TenantIdentifier tenantIdentifier, String className, String userId) + throws StorageQueryException { + // TODO.. // check if the input userId is being used in nonAuthRecipes. if (className.equals(SessionStorage.class.getName())) { String[] sessionHandlesForUser = getAllNonExpiredSessionHandlesForUser(userId); diff --git a/src/test/java/io/supertokens/storage/postgresql/test/DeadlockTest.java b/src/test/java/io/supertokens/storage/postgresql/test/DeadlockTest.java index 81ec80bc..03c4bab3 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/DeadlockTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/DeadlockTest.java @@ -19,11 +19,11 @@ import io.supertokens.ProcessState; import io.supertokens.passwordless.Passwordless; -import io.supertokens.passwordless.exceptions.RestartFlowException; import io.supertokens.pluginInterface.KeyValueInfo; import io.supertokens.pluginInterface.Storage; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.sqlStorage.SQLStorage; import io.supertokens.storageLayer.StorageLayer; import org.junit.AfterClass; @@ -38,9 +38,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; public class DeadlockTest { @Rule @@ -59,15 +57,17 @@ public void beforeEach() { @Test public void transactionDeadlockTesting() throws InterruptedException, StorageQueryException, StorageTransactionLogicException { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); Storage storage = StorageLayer.getStorage(process.getProcess()); SQLStorage sqlStorage = (SQLStorage) storage; sqlStorage.startTransaction(con -> { - sqlStorage.setKeyValue_Transaction(con, "Key", new KeyValueInfo("Value")); - sqlStorage.setKeyValue_Transaction(con, "Key1", new KeyValueInfo("Value1")); + sqlStorage.setKeyValue_Transaction(new TenantIdentifier(null, null, null), con, "Key", + new KeyValueInfo("Value")); + sqlStorage.setKeyValue_Transaction(new TenantIdentifier(null, null, null), con, "Key1", + new KeyValueInfo("Value1")); sqlStorage.commitTransaction(con); return null; }); @@ -83,7 +83,7 @@ public void transactionDeadlockTesting() try { sqlStorage.startTransaction(con -> { - sqlStorage.getKeyValue_Transaction(con, "Key"); + sqlStorage.getKeyValue_Transaction(new TenantIdentifier(null, null, null), con, "Key"); synchronized (syncObject) { t1State.set("read"); @@ -99,7 +99,7 @@ public void transactionDeadlockTesting() } } - sqlStorage.getKeyValue_Transaction(con, "Key1"); + sqlStorage.getKeyValue_Transaction(new TenantIdentifier(null, null, null), con, "Key1"); t1Failed.set(false); // it should come here because we will try three times. return null; }); @@ -111,7 +111,7 @@ public void transactionDeadlockTesting() try { sqlStorage.startTransaction(con -> { - sqlStorage.getKeyValue_Transaction(con, "Key1"); + sqlStorage.getKeyValue_Transaction(new TenantIdentifier(null, null, null), con, "Key1"); synchronized (syncObject) { t2State.set("read"); @@ -127,7 +127,7 @@ public void transactionDeadlockTesting() } } - sqlStorage.getKeyValue_Transaction(con, "Key"); + sqlStorage.getKeyValue_Transaction(new TenantIdentifier(null, null, null), con, "Key"); t2Failed.set(false); // it should come here because we will try three times. return null; @@ -155,7 +155,7 @@ public void transactionDeadlockTesting() @Test public void testCodeCreationRapidly() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -191,7 +191,7 @@ public void testCodeCreationRapidly() throws Exception { @Test public void testCodeCreationRapidlyWithDifferentEmails() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); diff --git a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java index 552d17c4..14ddbce0 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java @@ -20,13 +20,13 @@ import com.google.gson.JsonPrimitive; import io.supertokens.ProcessState; import io.supertokens.config.Config; -import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.featureflag.EE_FEATURES; import io.supertokens.featureflag.FeatureFlagTestContent; import io.supertokens.pluginInterface.Storage; import io.supertokens.pluginInterface.exceptions.DbInitException; import io.supertokens.pluginInterface.exceptions.InvalidConfigException; import io.supertokens.pluginInterface.multitenancy.*; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.test.TestingProcessManager; import io.supertokens.storage.postgresql.test.Utils; @@ -175,7 +175,7 @@ public void storageInstanceIsReusedAcrossTenants() tenantConfig.add("access_token_validity", new JsonPrimitive(3601)); TenantConfig[] tenants = new TenantConfig[]{ - new TenantConfig(new TenantIdentifier("abc", null, null), new EmailPasswordConfig(false), + new TenantConfig(new TenantIdentifier(null, "abc", null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), tenantConfig)}; @@ -184,14 +184,14 @@ public void storageInstanceIsReusedAcrossTenants() StorageLayer.loadAllTenantStorage(process.getProcess(), tenants); - assertSame(StorageLayer.getStorage(new TenantIdentifier("abc", null, null), process.getProcess()), + assertSame(StorageLayer.getStorage(new TenantIdentifier(null, "abc", null), process.getProcess()), StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess())); Assert.assertEquals( Config.getConfig(new TenantIdentifier(null, null, null), process.getProcess()).getAccessTokenValidity(), (long) 3600 * 1000); - Assert.assertEquals(Config.getConfig(new TenantIdentifier("abc", null, null), process.getProcess()) + Assert.assertEquals(Config.getConfig(new TenantIdentifier(null, "abc", null), process.getProcess()) .getAccessTokenValidity(), (long) 3601 * 1000); @@ -222,11 +222,11 @@ public void storageInstanceIsReusedAcrossTenantsComplex() tenantConfig1.add("postgresql_connection_pool_size", new JsonPrimitive(11)); TenantConfig[] tenants = new TenantConfig[]{ - new TenantConfig(new TenantIdentifier("abc", null, null), new EmailPasswordConfig(false), + new TenantConfig(new TenantIdentifier(null, "abc", null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), tenantConfig), - new TenantConfig(new TenantIdentifier("abc", null, "t1"), new EmailPasswordConfig(false), + new TenantConfig(new TenantIdentifier(null, "abc", "t1"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), tenantConfig1), @@ -239,20 +239,20 @@ public void storageInstanceIsReusedAcrossTenantsComplex() StorageLayer.loadAllTenantStorage(process.getProcess(), tenants); - assertSame(StorageLayer.getStorage(new TenantIdentifier("abc", null, null), process.getProcess()), + assertSame(StorageLayer.getStorage(new TenantIdentifier(null, "abc", null), process.getProcess()), StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess())); - assertSame(StorageLayer.getStorage(new TenantIdentifier("abc", null, "t1"), process.getProcess()), + assertSame(StorageLayer.getStorage(new TenantIdentifier(null, "abc", "t1"), process.getProcess()), StorageLayer.getStorage(new TenantIdentifier(null, null, "t2"), process.getProcess())); - assertNotSame(StorageLayer.getStorage(new TenantIdentifier("abc", null, "t1"), process.getProcess()), + assertNotSame(StorageLayer.getStorage(new TenantIdentifier(null, "abc", "t1"), process.getProcess()), StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess())); Assert.assertEquals( Config.getConfig(new TenantIdentifier(null, null, null), process.getProcess()).getAccessTokenValidity(), (long) 3600 * 1000); - Assert.assertEquals(Config.getConfig(new TenantIdentifier("abc", null, null), process.getProcess()) + Assert.assertEquals(Config.getConfig(new TenantIdentifier(null, "abc", null), process.getProcess()) .getAccessTokenValidity(), (long) 3601 * 1000); @@ -261,7 +261,7 @@ public void storageInstanceIsReusedAcrossTenantsComplex() .size(), 4); Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( - (Start) StorageLayer.getStorage(new TenantIdentifier("abc", null, "t1"), process.getProcess())) + (Start) StorageLayer.getStorage(new TenantIdentifier(null, "abc", "t1"), process.getProcess())) .getConnectionPoolSize(), 11); Assert.assertEquals(io.supertokens.storage.postgresql.config.Config.getConfig( @@ -327,7 +327,7 @@ public void mergingTenantWithBaseConfigWithConflictingConfigsThrowsError() try { TenantConfig[] tenants = new TenantConfig[]{ - new TenantConfig(new TenantIdentifier("abc", null, null), new EmailPasswordConfig(false), + new TenantConfig(new TenantIdentifier(null, "abc", null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), tenantConfig)}; @@ -361,7 +361,7 @@ public void mergingDifferentConnectionPoolIdTenantWithBaseConfigWithConflictingC try { TenantConfig[] tenants = new TenantConfig[]{ - new TenantConfig(new TenantIdentifier("abc", null, null), new EmailPasswordConfig(false), + new TenantConfig(new TenantIdentifier(null, "abc", null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), tenantConfig)}; @@ -446,7 +446,7 @@ public void newStorageIsNotCreatedWhenSameTenantIsAdded() tenantConfig.add("access_token_validity", new JsonPrimitive(3601)); TenantConfig[] tenants = new TenantConfig[]{ - new TenantConfig(new TenantIdentifier("abc", null, null), new EmailPasswordConfig(false), + new TenantConfig(new TenantIdentifier(null, "abc", null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), tenantConfig)}; @@ -455,14 +455,14 @@ public void newStorageIsNotCreatedWhenSameTenantIsAdded() StorageLayer.loadAllTenantStorage(process.getProcess(), tenants); - assertSame(StorageLayer.getStorage(new TenantIdentifier("abc", null, null), process.getProcess()), + assertSame(StorageLayer.getStorage(new TenantIdentifier(null, "abc", null), process.getProcess()), existingStorage); Assert.assertEquals( Config.getConfig(new TenantIdentifier(null, null, null), process.getProcess()).getAccessTokenValidity(), (long) 3600 * 1000); - Assert.assertEquals(Config.getConfig(new TenantIdentifier("abc", null, null), process.getProcess()) + Assert.assertEquals(Config.getConfig(new TenantIdentifier(null, "abc", null), process.getProcess()) .getAccessTokenValidity(), (long) 3601 * 1000); @@ -490,6 +490,8 @@ public void testDifferentWaysToGetConfigBasedOnConnectionURIAndTenantId() { JsonObject tenantConfig = new JsonObject(); + StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess()) + .modifyConfigToAddANewUserPoolForTesting(tenantConfig, 2); tenantConfig.add("postgresql_connection_pool_size", new JsonPrimitive(12)); tenants[0] = new TenantConfig(new TenantIdentifier("c1", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), @@ -499,6 +501,8 @@ public void testDifferentWaysToGetConfigBasedOnConnectionURIAndTenantId() { JsonObject tenantConfig = new JsonObject(); + StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess()) + .modifyConfigToAddANewUserPoolForTesting(tenantConfig, 2); tenantConfig.add("postgresql_connection_pool_size", new JsonPrimitive(13)); tenants[1] = new TenantConfig(new TenantIdentifier("c1", null, "t1"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), @@ -657,7 +661,7 @@ public void multipleTenantsSameUserPoolAndConnectionPoolShouldWork() JsonObject tenantConfig = new JsonObject(); TenantConfig[] tenants = new TenantConfig[]{ - new TenantConfig(new TenantIdentifier("abc", null, null), new EmailPasswordConfig(false), + new TenantConfig(new TenantIdentifier(null, "abc", null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), tenantConfig)}; @@ -665,7 +669,7 @@ public void multipleTenantsSameUserPoolAndConnectionPoolShouldWork() StorageLayer.loadAllTenantStorage(process.getProcess(), tenants); - assertSame(StorageLayer.getStorage(new TenantIdentifier("abc", null, null), process.getProcess()), + assertSame(StorageLayer.getStorage(new TenantIdentifier(null, "abc", null), process.getProcess()), StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess())); Assert.assertEquals( @@ -692,7 +696,7 @@ public void multipleTenantsSameUserPoolAndDifferentConnectionPoolShouldWork() tenantConfig.add("postgresql_connection_pool_size", new JsonPrimitive(20)); TenantConfig[] tenants = new TenantConfig[]{ - new TenantConfig(new TenantIdentifier("abc", null, null), new EmailPasswordConfig(false), + new TenantConfig(new TenantIdentifier(null, "abc", null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), tenantConfig)}; @@ -700,7 +704,7 @@ public void multipleTenantsSameUserPoolAndDifferentConnectionPoolShouldWork() StorageLayer.loadAllTenantStorage(process.getProcess(), tenants); - assertNotSame(StorageLayer.getStorage(new TenantIdentifier("abc", null, null), process.getProcess()), + assertNotSame(StorageLayer.getStorage(new TenantIdentifier(null, "abc", null), process.getProcess()), StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess())); Assert.assertEquals( From 5a9a47d301a59df5707d9ba78319318a56787c40 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 14 Feb 2023 22:53:21 +0530 Subject: [PATCH 027/106] changes to session receipe to add tenantIdentifier --- .../supertokens/storage/postgresql/Start.java | 66 ++++++++++++++----- 1 file changed, 48 insertions(+), 18 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index be178a74..3607e7f1 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -323,8 +323,10 @@ public void commitTransaction(TransactionConnection con) throws StorageQueryExce } @Override - public KeyValueInfo getLegacyAccessTokenSigningKey_Transaction(TransactionConnection con) + public KeyValueInfo getLegacyAccessTokenSigningKey_Transaction(TenantIdentifier tenantIdentifier, + TransactionConnection con) throws StorageQueryException { + // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { return GeneralQueries.getKeyValue_Transaction(this, sqlCon, ACCESS_TOKEN_SIGNING_KEY_NAME); @@ -334,7 +336,9 @@ public KeyValueInfo getLegacyAccessTokenSigningKey_Transaction(TransactionConnec } @Override - public void removeLegacyAccessTokenSigningKey_Transaction(TransactionConnection con) throws StorageQueryException { + public void removeLegacyAccessTokenSigningKey_Transaction(TenantIdentifier tenantIdentifier, + TransactionConnection con) throws StorageQueryException { + // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { GeneralQueries.deleteKeyValue_Transaction(this, sqlCon, ACCESS_TOKEN_SIGNING_KEY_NAME); @@ -344,8 +348,10 @@ public void removeLegacyAccessTokenSigningKey_Transaction(TransactionConnection } @Override - public KeyValueInfo[] getAccessTokenSigningKeys_Transaction(TransactionConnection con) + public KeyValueInfo[] getAccessTokenSigningKeys_Transaction(TenantIdentifier tenantIdentifier, + TransactionConnection con) throws StorageQueryException { + // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { return SessionQueries.getAccessTokenSigningKeys_Transaction(this, sqlCon); @@ -355,8 +361,10 @@ public KeyValueInfo[] getAccessTokenSigningKeys_Transaction(TransactionConnectio } @Override - public void addAccessTokenSigningKey_Transaction(TransactionConnection con, KeyValueInfo info) + public void addAccessTokenSigningKey_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, + KeyValueInfo info) throws StorageQueryException { + // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { SessionQueries.addAccessTokenSigningKey_Transaction(this, sqlCon, info.createdAtTime, info.value); @@ -366,8 +374,10 @@ public void addAccessTokenSigningKey_Transaction(TransactionConnection con, KeyV } @Override - public void removeAccessTokenSigningKeysBefore(long time) throws StorageQueryException { + public void removeAccessTokenSigningKeysBefore(TenantIdentifier tenantIdentifier, long time) + throws StorageQueryException { try { + // TODO.. SessionQueries.removeAccessTokenSigningKeysBefore(this, time); } catch (SQLException e) { throw new StorageQueryException(e); @@ -375,7 +385,9 @@ public void removeAccessTokenSigningKeysBefore(long time) throws StorageQueryExc } @Override - public KeyValueInfo getRefreshTokenSigningKey_Transaction(TransactionConnection con) throws StorageQueryException { + public KeyValueInfo getRefreshTokenSigningKey_Transaction(TenantIdentifier tenantIdentifier, + TransactionConnection con) throws StorageQueryException { + // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { return GeneralQueries.getKeyValue_Transaction(this, sqlCon, REFRESH_TOKEN_KEY_NAME); @@ -385,8 +397,10 @@ public KeyValueInfo getRefreshTokenSigningKey_Transaction(TransactionConnection } @Override - public void setRefreshTokenSigningKey_Transaction(TransactionConnection con, KeyValueInfo info) + public void setRefreshTokenSigningKey_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, + KeyValueInfo info) throws StorageQueryException { + // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { GeneralQueries.setKeyValue_Transaction(this, sqlCon, REFRESH_TOKEN_KEY_NAME, info); @@ -411,10 +425,12 @@ public void close() { } @Override - public void createNewSession(String sessionHandle, String userId, String refreshTokenHash2, + public void createNewSession(TenantIdentifier tenantIdentifier, String sessionHandle, String userId, + String refreshTokenHash2, JsonObject userDataInDatabase, long expiry, JsonObject userDataInJWT, long createdAtTime) throws StorageQueryException { + // TODO.. try { SessionQueries.createNewSession(this, sessionHandle, userId, refreshTokenHash2, userDataInDatabase, expiry, userDataInJWT, createdAtTime); @@ -424,8 +440,9 @@ public void createNewSession(String sessionHandle, String userId, String refresh } @Override - public void deleteSessionsOfUser(String userId) throws StorageQueryException { + public void deleteSessionsOfUser(TenantIdentifier tenantIdentifier, String userId) throws StorageQueryException { try { + // TODO.. SessionQueries.deleteSessionsOfUser(this, userId); } catch (SQLException e) { throw new StorageQueryException(e); @@ -433,8 +450,9 @@ public void deleteSessionsOfUser(String userId) throws StorageQueryException { } @Override - public int getNumberOfSessions() throws StorageQueryException { + public int getNumberOfSessions(TenantIdentifier tenantIdentifier) throws StorageQueryException { try { + // TODO.. return SessionQueries.getNumberOfSessions(this); } catch (SQLException e) { throw new StorageQueryException(e); @@ -442,8 +460,9 @@ public int getNumberOfSessions() throws StorageQueryException { } @Override - public int deleteSession(String[] sessionHandles) throws StorageQueryException { + public int deleteSession(TenantIdentifier tenantIdentifier, String[] sessionHandles) throws StorageQueryException { try { + // TODO.. return SessionQueries.deleteSession(this, sessionHandles); } catch (SQLException e) { throw new StorageQueryException(e); @@ -451,8 +470,10 @@ public int deleteSession(String[] sessionHandles) throws StorageQueryException { } @Override - public String[] getAllNonExpiredSessionHandlesForUser(String userId) throws StorageQueryException { + public String[] getAllNonExpiredSessionHandlesForUser(TenantIdentifier tenantIdentifier, String userId) + throws StorageQueryException { try { + // TODO.. return SessionQueries.getAllNonExpiredSessionHandlesForUser(this, userId); } catch (SQLException e) { throw new StorageQueryException(e); @@ -495,8 +516,10 @@ public void setStorageLayerEnabled(boolean enabled) { } @Override - public SessionInfo getSession(String sessionHandle) throws StorageQueryException { + public SessionInfo getSession(TenantIdentifier tenantIdentifier, String sessionHandle) + throws StorageQueryException { try { + // TODO.. return SessionQueries.getSession(this, sessionHandle); } catch (SQLException e) { throw new StorageQueryException(e); @@ -504,9 +527,11 @@ public SessionInfo getSession(String sessionHandle) throws StorageQueryException } @Override - public int updateSession(String sessionHandle, JsonObject sessionData, JsonObject jwtPayload) + public int updateSession(TenantIdentifier tenantIdentifier, String sessionHandle, JsonObject sessionData, + JsonObject jwtPayload) throws StorageQueryException { try { + // TODO.. return SessionQueries.updateSession(this, sessionHandle, sessionData, jwtPayload); } catch (SQLException e) { throw new StorageQueryException(e); @@ -514,8 +539,10 @@ public int updateSession(String sessionHandle, JsonObject sessionData, JsonObjec } @Override - public SessionInfo getSessionInfo_Transaction(TransactionConnection con, String sessionHandle) + public SessionInfo getSessionInfo_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, + String sessionHandle) throws StorageQueryException { + // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { return SessionQueries.getSessionInfo_Transaction(this, sqlCon, sessionHandle); @@ -525,10 +552,12 @@ public SessionInfo getSessionInfo_Transaction(TransactionConnection con, String } @Override - public void updateSessionInfo_Transaction(TransactionConnection con, String sessionHandle, String refreshTokenHash2, + public void updateSessionInfo_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, + String sessionHandle, String refreshTokenHash2, long expiry) throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { + // TODO.. SessionQueries.updateSessionInfo_Transaction(this, sqlCon, sessionHandle, refreshTokenHash2, expiry); } catch (SQLException e) { throw new StorageQueryException(e); @@ -591,7 +620,7 @@ public boolean isUserIdBeingUsedInNonAuthRecipe(TenantIdentifier tenantIdentifie // TODO.. // check if the input userId is being used in nonAuthRecipes. if (className.equals(SessionStorage.class.getName())) { - String[] sessionHandlesForUser = getAllNonExpiredSessionHandlesForUser(userId); + String[] sessionHandlesForUser = getAllNonExpiredSessionHandlesForUser(tenantIdentifier, userId); return sessionHandlesForUser.length > 0; } else if (className.equals(UserRolesStorage.class.getName())) { String[] roles = getRolesForUser(userId); @@ -618,7 +647,8 @@ public void addInfoToNonAuthRecipesBasedOnUserId(String className, String userId // add entries to nonAuthRecipe tables with input userId if (className.equals(SessionStorage.class.getName())) { try { - createNewSession("sessionHandle", userId, "refreshTokenHash", new JsonObject(), + createNewSession(new TenantIdentifier(null, null, null), "sessionHandle", userId, "refreshTokenHash", + new JsonObject(), System.currentTimeMillis() + 1000000, new JsonObject(), System.currentTimeMillis()); } catch (Exception e) { throw new StorageQueryException(e); From 1c39f03baa51c114fd84a4589247663f85228520 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Wed, 15 Feb 2023 14:47:38 +0530 Subject: [PATCH 028/106] introduces the concept of appIdentifier vs tenantIdentifier --- .../supertokens/storage/postgresql/Start.java | 50 ++++++++++++------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 3607e7f1..f02be5c1 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -44,6 +44,7 @@ import io.supertokens.pluginInterface.jwt.JWTSigningKeyInfo; import io.supertokens.pluginInterface.jwt.exceptions.DuplicateKeyIdException; import io.supertokens.pluginInterface.jwt.sqlstorage.JWTRecipeSQLStorage; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.MultitenancyStorage; import io.supertokens.pluginInterface.multitenancy.TenantConfig; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; @@ -323,7 +324,7 @@ public void commitTransaction(TransactionConnection con) throws StorageQueryExce } @Override - public KeyValueInfo getLegacyAccessTokenSigningKey_Transaction(TenantIdentifier tenantIdentifier, + public KeyValueInfo getLegacyAccessTokenSigningKey_Transaction(AppIdentifier appIdentifier, TransactionConnection con) throws StorageQueryException { // TODO.. @@ -336,7 +337,7 @@ public KeyValueInfo getLegacyAccessTokenSigningKey_Transaction(TenantIdentifier } @Override - public void removeLegacyAccessTokenSigningKey_Transaction(TenantIdentifier tenantIdentifier, + public void removeLegacyAccessTokenSigningKey_Transaction(AppIdentifier appIdentifier, TransactionConnection con) throws StorageQueryException { // TODO.. Connection sqlCon = (Connection) con.getConnection(); @@ -348,7 +349,7 @@ public void removeLegacyAccessTokenSigningKey_Transaction(TenantIdentifier tenan } @Override - public KeyValueInfo[] getAccessTokenSigningKeys_Transaction(TenantIdentifier tenantIdentifier, + public KeyValueInfo[] getAccessTokenSigningKeys_Transaction(AppIdentifier appIdentifier, TransactionConnection con) throws StorageQueryException { // TODO.. @@ -361,7 +362,7 @@ public KeyValueInfo[] getAccessTokenSigningKeys_Transaction(TenantIdentifier ten } @Override - public void addAccessTokenSigningKey_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, + public void addAccessTokenSigningKey_Transaction(AppIdentifier appIdentifier, TransactionConnection con, KeyValueInfo info) throws StorageQueryException { // TODO.. @@ -374,7 +375,7 @@ public void addAccessTokenSigningKey_Transaction(TenantIdentifier tenantIdentifi } @Override - public void removeAccessTokenSigningKeysBefore(TenantIdentifier tenantIdentifier, long time) + public void removeAccessTokenSigningKeysBefore(AppIdentifier appIdentifier, long time) throws StorageQueryException { try { // TODO.. @@ -385,7 +386,7 @@ public void removeAccessTokenSigningKeysBefore(TenantIdentifier tenantIdentifier } @Override - public KeyValueInfo getRefreshTokenSigningKey_Transaction(TenantIdentifier tenantIdentifier, + public KeyValueInfo getRefreshTokenSigningKey_Transaction(AppIdentifier appIdentifier, TransactionConnection con) throws StorageQueryException { // TODO.. Connection sqlCon = (Connection) con.getConnection(); @@ -397,7 +398,7 @@ public KeyValueInfo getRefreshTokenSigningKey_Transaction(TenantIdentifier tenan } @Override - public void setRefreshTokenSigningKey_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, + public void setRefreshTokenSigningKey_Transaction(AppIdentifier appIdentifier, TransactionConnection con, KeyValueInfo info) throws StorageQueryException { // TODO.. @@ -480,6 +481,16 @@ public String[] getAllNonExpiredSessionHandlesForUser(TenantIdentifier tenantIde } } + private String[] getAllNonExpiredSessionHandlesForUser(AppIdentifier appIdentifier, String userId) + throws StorageQueryException { + try { + // TODO.. + return SessionQueries.getAllNonExpiredSessionHandlesForUser(this, userId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + @Override public void deleteAllExpiredSessions() throws StorageQueryException { try { @@ -615,12 +626,12 @@ public boolean canBeUsed(JsonObject configJson) { } @Override - public boolean isUserIdBeingUsedInNonAuthRecipe(TenantIdentifier tenantIdentifier, String className, String userId) + public boolean isUserIdBeingUsedInNonAuthRecipe(AppIdentifier appIdentifier, String className, String userId) throws StorageQueryException { // TODO.. // check if the input userId is being used in nonAuthRecipes. if (className.equals(SessionStorage.class.getName())) { - String[] sessionHandlesForUser = getAllNonExpiredSessionHandlesForUser(tenantIdentifier, userId); + String[] sessionHandlesForUser = getAllNonExpiredSessionHandlesForUser(appIdentifier, userId); return sessionHandlesForUser.length > 0; } else if (className.equals(UserRolesStorage.class.getName())) { String[] roles = getRolesForUser(userId); @@ -1185,7 +1196,7 @@ public AuthRecipeUserInfo[] getUsers(TenantIdentifier tenantIdentifier, @NotNull } @Override - public boolean doesUserIdExist(TenantIdentifier tenantIdentifier, String userId) throws StorageQueryException { + public boolean doesUserIdExist(AppIdentifier appIdentifier, String userId) throws StorageQueryException { // TODO.. try { return GeneralQueries.doesUserIdExist(this, userId); @@ -1195,8 +1206,9 @@ public boolean doesUserIdExist(TenantIdentifier tenantIdentifier, String userId) } @Override - public List getJWTSigningKeys_Transaction(TransactionConnection con) + public List getJWTSigningKeys_Transaction(AppIdentifier appIdentifier, TransactionConnection con) throws StorageQueryException { + // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { return JWTSigningQueries.getJWTSigningKeys_Transaction(this, sqlCon); @@ -1206,8 +1218,10 @@ public List getJWTSigningKeys_Transaction(TransactionConnecti } @Override - public void setJWTSigningKey_Transaction(TransactionConnection con, JWTSigningKeyInfo info) + public void setJWTSigningKey_Transaction(AppIdentifier appIdentifier, TransactionConnection con, + JWTSigningKeyInfo info) throws StorageQueryException, DuplicateKeyIdException { + // TODO... Connection sqlCon = (Connection) con.getConnection(); try { JWTSigningQueries.setJWTSigningKeyInfo_Transaction(this, sqlCon, info); @@ -1788,7 +1802,7 @@ public boolean doesRoleExist_Transaction(TransactionConnection con, String role) } @Override - public void createUserIdMapping(TenantIdentifier tenantIdentifier, String superTokensUserId, String externalUserId, + public void createUserIdMapping(AppIdentifier appIdentifier, String superTokensUserId, String externalUserId, @Nullable String externalUserIdInfo) throws StorageQueryException, UnknownSuperTokensUserIdException, UserIdMappingAlreadyExistsException { // TODO.. @@ -1822,7 +1836,7 @@ public void createUserIdMapping(TenantIdentifier tenantIdentifier, String superT } @Override - public boolean deleteUserIdMapping(TenantIdentifier tenantIdentifier, String userId, boolean isSuperTokensUserId) + public boolean deleteUserIdMapping(AppIdentifier appIdentifier, String userId, boolean isSuperTokensUserId) throws StorageQueryException { // TODO.. try { @@ -1837,7 +1851,7 @@ public boolean deleteUserIdMapping(TenantIdentifier tenantIdentifier, String use } @Override - public UserIdMapping getUserIdMapping(TenantIdentifier tenantIdentifier, String userId, boolean isSuperTokensUserId) + public UserIdMapping getUserIdMapping(AppIdentifier appIdentifier, String userId, boolean isSuperTokensUserId) throws StorageQueryException { // TODO.. try { @@ -1852,7 +1866,7 @@ public UserIdMapping getUserIdMapping(TenantIdentifier tenantIdentifier, String } @Override - public UserIdMapping[] getUserIdMapping(TenantIdentifier tenantIdentifier, String userId) + public UserIdMapping[] getUserIdMapping(AppIdentifier appIdentifier, String userId) throws StorageQueryException { // TODO.. try { @@ -1863,7 +1877,7 @@ public UserIdMapping[] getUserIdMapping(TenantIdentifier tenantIdentifier, Strin } @Override - public boolean updateOrDeleteExternalUserIdInfo(TenantIdentifier tenantIdentifier, String userId, + public boolean updateOrDeleteExternalUserIdInfo(AppIdentifier appIdentifier, String userId, boolean isSuperTokensUserId, @Nullable String externalUserIdInfo) throws StorageQueryException { @@ -1882,7 +1896,7 @@ public boolean updateOrDeleteExternalUserIdInfo(TenantIdentifier tenantIdentifie } @Override - public HashMap getUserIdMappingForSuperTokensIds(TenantIdentifier tenantIdentifier, + public HashMap getUserIdMappingForSuperTokensIds(AppIdentifier appIdentifier, ArrayList userIds) throws StorageQueryException { // TODO.. From 67463295f910b378131e2cd31a1b473a026ad38a Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Wed, 15 Feb 2023 17:59:55 +0530 Subject: [PATCH 029/106] fixes test compilation issues --- .../supertokens/storage/postgresql/Start.java | 14 +++++++++++++- .../storage/postgresql/test/ConfigTest.java | 13 +++++++++---- .../postgresql/test/ExceptionParsingTest.java | 5 +++-- .../postgresql/test/InMemoryDBTest.java | 19 +++++++++++++------ 4 files changed, 38 insertions(+), 13 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index f02be5c1..4f3fd76c 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -441,7 +441,8 @@ public void createNewSession(TenantIdentifier tenantIdentifier, String sessionHa } @Override - public void deleteSessionsOfUser(TenantIdentifier tenantIdentifier, String userId) throws StorageQueryException { + public void deleteSessionsOfUser(AppIdentifier appIdentifierIdentifier, String userId) + throws StorageQueryException { try { // TODO.. SessionQueries.deleteSessionsOfUser(this, userId); @@ -1205,6 +1206,17 @@ public boolean doesUserIdExist(AppIdentifier appIdentifier, String userId) throw } } + @Override + public boolean doesUserIdExist(TenantIdentifier tenantIdentifierIdentifier, String userId) + throws StorageQueryException { + // TODO:... + try { + return GeneralQueries.doesUserIdExist(this, userId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + @Override public List getJWTSigningKeys_Transaction(AppIdentifier appIdentifier, TransactionConnection con) throws StorageQueryException { diff --git a/src/test/java/io/supertokens/storage/postgresql/test/ConfigTest.java b/src/test/java/io/supertokens/storage/postgresql/test/ConfigTest.java index 14ef81bc..7ba054c0 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/ConfigTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/ConfigTest.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.google.gson.JsonObject; import io.supertokens.ProcessState; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.session.Session; import io.supertokens.session.info.SessionInformationHolder; import io.supertokens.storage.postgresql.ConnectionPoolTestContent; @@ -310,7 +311,8 @@ public void testAddingSchemaWorks() throws Exception { assert sessionInfo.accessToken != null; assert sessionInfo.refreshToken != null; - TestCase.assertEquals(StorageLayer.getSessionStorage(process.getProcess()).getNumberOfSessions(), 1); + TestCase.assertEquals(StorageLayer.getSessionStorage(process.getProcess()) + .getNumberOfSessions(new TenantIdentifier(null, null, null)), 1); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -353,7 +355,8 @@ public void testAddingSchemaViaConnectionUriWorks() throws Exception { assert sessionInfo.accessToken != null; assert sessionInfo.refreshToken != null; - TestCase.assertEquals(StorageLayer.getSessionStorage(process.getProcess()).getNumberOfSessions(), 1); + TestCase.assertEquals(StorageLayer.getSessionStorage(process.getProcess()) + .getNumberOfSessions(new TenantIdentifier(null, null, null)), 1); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -396,7 +399,8 @@ public void testAddingSchemaViaConnectionUriWorks2() throws Exception { assert sessionInfo.accessToken != null; assert sessionInfo.refreshToken != null; - TestCase.assertEquals(StorageLayer.getSessionStorage(process.getProcess()).getNumberOfSessions(), 1); + TestCase.assertEquals(StorageLayer.getSessionStorage(process.getProcess()) + .getNumberOfSessions(new TenantIdentifier(null, null, null)), 1); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -439,7 +443,8 @@ public void testAddingSchemaViaConnectionUriWorks3() throws Exception { assert sessionInfo.accessToken != null; assert sessionInfo.refreshToken != null; - TestCase.assertEquals(StorageLayer.getSessionStorage(process.getProcess()).getNumberOfSessions(), 1); + TestCase.assertEquals(StorageLayer.getSessionStorage(process.getProcess()) + .getNumberOfSessions(new TenantIdentifier(null, null, null)), 1); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); diff --git a/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java b/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java index ce80e26c..ff789af4 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java @@ -32,6 +32,7 @@ import io.supertokens.pluginInterface.jwt.JWTSymmetricSigningKeyInfo; import io.supertokens.pluginInterface.jwt.exceptions.DuplicateKeyIdException; import io.supertokens.pluginInterface.jwt.sqlstorage.JWTRecipeSQLStorage; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.thirdparty.exception.DuplicateThirdPartyUserException; import io.supertokens.storageLayer.StorageLayer; @@ -374,13 +375,13 @@ public void setJWTSigningKey_TransactionExceptions() throws InterruptedException var info = new JWTSymmetricSigningKeyInfo(keyId, System.currentTimeMillis(), algorithm, keyString); storage.startTransaction(con -> { try { - storage.setJWTSigningKey_Transaction(con, info); + storage.setJWTSigningKey_Transaction(new AppIdentifier(null, null), con, info); } catch (DuplicateKeyIdException e) { throw new StorageTransactionLogicException(e); } try { - storage.setJWTSigningKey_Transaction(con, info); + storage.setJWTSigningKey_Transaction(new AppIdentifier(null, null), con, info); throw new StorageTransactionLogicException(new Exception("This should throw")); } catch (DuplicateKeyIdException e) { // expected diff --git a/src/test/java/io/supertokens/storage/postgresql/test/InMemoryDBTest.java b/src/test/java/io/supertokens/storage/postgresql/test/InMemoryDBTest.java index 64773b62..92727b4e 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/InMemoryDBTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/InMemoryDBTest.java @@ -21,6 +21,7 @@ import io.supertokens.ProcessState; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.session.Session; import io.supertokens.session.info.SessionInformationHolder; import io.supertokens.storageLayer.StorageLayer; @@ -84,7 +85,8 @@ public void checkThatInMemDVWorksEvenIfWrongConfig() assert sessionInfo.accessToken != null; assert sessionInfo.refreshToken != null; - assertEquals(StorageLayer.getSessionStorage(process.getProcess()).getNumberOfSessions(), 1); + assertEquals(StorageLayer.getSessionStorage(process.getProcess()) + .getNumberOfSessions(new TenantIdentifier(null, null, null)), 1); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -96,7 +98,8 @@ public void checkThatInMemDVWorksEvenIfWrongConfig() TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - assertEquals(StorageLayer.getSessionStorage(process.getProcess()).getNumberOfSessions(), 0); + assertEquals(StorageLayer.getSessionStorage(process.getProcess()) + .getNumberOfSessions(new TenantIdentifier(null, null, null)), 0); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -125,7 +128,8 @@ public void checkThatActualDBWorksIfCorrectConfigDev() throws InterruptedExcepti assert sessionInfo.accessToken != null; assert sessionInfo.refreshToken != null; - assertEquals(StorageLayer.getSessionStorage(process.getProcess()).getNumberOfSessions(), 1); + assertEquals(StorageLayer.getSessionStorage(process.getProcess()) + .getNumberOfSessions(new TenantIdentifier(null, null, null)), 1); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -135,7 +139,8 @@ public void checkThatActualDBWorksIfCorrectConfigDev() throws InterruptedExcepti TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - assertEquals(StorageLayer.getSessionStorage(process.getProcess()).getNumberOfSessions(), 1); + assertEquals(StorageLayer.getSessionStorage(process.getProcess()) + .getNumberOfSessions(new TenantIdentifier(null, null, null)), 1); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -164,7 +169,8 @@ public void checkThatActualDBWorksIfCorrectConfigProduction() throws Interrupted assert sessionInfo.accessToken != null; assert sessionInfo.refreshToken != null; - assertEquals(StorageLayer.getSessionStorage(process.getProcess()).getNumberOfSessions(), 1); + assertEquals(StorageLayer.getSessionStorage(process.getProcess()) + .getNumberOfSessions(new TenantIdentifier(null, null, null)), 1); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -174,7 +180,8 @@ public void checkThatActualDBWorksIfCorrectConfigProduction() throws Interrupted TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - assertEquals(StorageLayer.getSessionStorage(process.getProcess()).getNumberOfSessions(), 1); + assertEquals(StorageLayer.getSessionStorage(process.getProcess()) + .getNumberOfSessions(new TenantIdentifier(null, null, null)), 1); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); From cb724ee7225a8914a3cd70000458d24d9c871b7e Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Wed, 15 Feb 2023 19:40:00 +0530 Subject: [PATCH 030/106] changes as per plugin change --- .../supertokens/storage/postgresql/Start.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 4f3fd76c..5cc63a7e 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -638,7 +638,7 @@ public boolean isUserIdBeingUsedInNonAuthRecipe(AppIdentifier appIdentifier, Str String[] roles = getRolesForUser(userId); return roles.length > 0; } else if (className.equals(UserMetadataStorage.class.getName())) { - JsonObject userMetadata = getUserMetadata(userId); + JsonObject userMetadata = getUserMetadata(appIdentifier, userId); return userMetadata != null; } else if (className.equals(EmailVerificationStorage.class.getName())) { try { @@ -694,7 +694,7 @@ public void addInfoToNonAuthRecipesBasedOnUserId(String className, String userId data.addProperty("test", "testData"); try { this.startTransaction(con -> { - setUserMetadata_Transaction(con, userId, data); + setUserMetadata_Transaction(new AppIdentifier(null, null), con, userId, data); return null; }); } catch (StorageTransactionLogicException e) { @@ -1603,8 +1603,9 @@ public io.supertokens.pluginInterface.passwordless.UserInfo getUserByPhoneNumber } @Override - public JsonObject getUserMetadata(String userId) throws StorageQueryException { + public JsonObject getUserMetadata(AppIdentifier appIdentifier, String userId) throws StorageQueryException { try { + // TODO.. return UserMetadataQueries.getUserMetadata(this, userId); } catch (SQLException e) { throw new StorageQueryException(e); @@ -1612,8 +1613,9 @@ public JsonObject getUserMetadata(String userId) throws StorageQueryException { } @Override - public JsonObject getUserMetadata_Transaction(TransactionConnection con, String userId) + public JsonObject getUserMetadata_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId) throws StorageQueryException { + // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { return UserMetadataQueries.getUserMetadata_Transaction(this, sqlCon, userId); @@ -1623,8 +1625,10 @@ public JsonObject getUserMetadata_Transaction(TransactionConnection con, String } @Override - public int setUserMetadata_Transaction(TransactionConnection con, String userId, JsonObject metadata) + public int setUserMetadata_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId, + JsonObject metadata) throws StorageQueryException { + // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { return UserMetadataQueries.setUserMetadata_Transaction(this, sqlCon, userId, metadata); @@ -1634,8 +1638,9 @@ public int setUserMetadata_Transaction(TransactionConnection con, String userId, } @Override - public int deleteUserMetadata(String userId) throws StorageQueryException { + public int deleteUserMetadata(AppIdentifier appIdentifier, String userId) throws StorageQueryException { try { + // TODO.. return UserMetadataQueries.deleteUserMetadata(this, userId); } catch (SQLException e) { throw new StorageQueryException(e); From 72f7ec1b7593813673d9a1e1e42138d7f0bfdc92 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Wed, 15 Feb 2023 20:45:48 +0530 Subject: [PATCH 031/106] modifes user roles functions to add tenantidentifier and appidentifiers --- .../supertokens/storage/postgresql/Start.java | 79 ++++++++++++++----- 1 file changed, 59 insertions(+), 20 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 5cc63a7e..9bcbf510 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -635,7 +635,7 @@ public boolean isUserIdBeingUsedInNonAuthRecipe(AppIdentifier appIdentifier, Str String[] sessionHandlesForUser = getAllNonExpiredSessionHandlesForUser(appIdentifier, userId); return sessionHandlesForUser.length > 0; } else if (className.equals(UserRolesStorage.class.getName())) { - String[] roles = getRolesForUser(userId); + String[] roles = getRolesForUser(appIdentifier, userId); return roles.length > 0; } else if (className.equals(UserMetadataStorage.class.getName())) { JsonObject userMetadata = getUserMetadata(appIdentifier, userId); @@ -669,11 +669,11 @@ public void addInfoToNonAuthRecipesBasedOnUserId(String className, String userId try { String role = "testRole"; this.startTransaction(con -> { - createNewRoleOrDoNothingIfExists_Transaction(con, role); + createNewRoleOrDoNothingIfExists_Transaction(new TenantIdentifier(null, null, null), con, role); return null; }); try { - addRoleToUser(userId, role); + addRoleToUser(new TenantIdentifier(null, null, null), userId, role); } catch (Exception e) { throw new StorageTransactionLogicException(e); } @@ -1648,9 +1648,9 @@ public int deleteUserMetadata(AppIdentifier appIdentifier, String userId) throws } @Override - public void addRoleToUser(String userId, String role) + public void addRoleToUser(TenantIdentifier tenantIdentifier, String userId, String role) throws StorageQueryException, UnknownRoleException, DuplicateUserRoleMappingException { - + // TODO... try { UserRolesQueries.addRoleToUser(this, userId, role); } catch (SQLException e) { @@ -1670,8 +1670,18 @@ public void addRoleToUser(String userId, String role) } @Override - public String[] getRolesForUser(String userId) throws StorageQueryException { + public String[] getRolesForUser(TenantIdentifier tenantIdentifier, String userId) throws StorageQueryException { + try { + // TODO.. + return UserRolesQueries.getRolesForUser(this, userId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + private String[] getRolesForUser(AppIdentifier appIdentifier, String userId) throws StorageQueryException { try { + // TODO.. return UserRolesQueries.getRolesForUser(this, userId); } catch (SQLException e) { throw new StorageQueryException(e); @@ -1679,8 +1689,9 @@ public String[] getRolesForUser(String userId) throws StorageQueryException { } @Override - public String[] getUsersForRole(String role) throws StorageQueryException { + public String[] getUsersForRole(TenantIdentifier tenantIdentifier, String role) throws StorageQueryException { try { + // TODO.. return UserRolesQueries.getUsersForRole(this, role); } catch (SQLException e) { throw new StorageQueryException(e); @@ -1688,8 +1699,9 @@ public String[] getUsersForRole(String role) throws StorageQueryException { } @Override - public String[] getPermissionsForRole(String role) throws StorageQueryException { + public String[] getPermissionsForRole(AppIdentifier appIdentifier, String role) throws StorageQueryException { try { + // TODO.. return UserRolesQueries.getPermissionsForRole(this, role); } catch (SQLException e) { throw new StorageQueryException(e); @@ -1697,8 +1709,10 @@ public String[] getPermissionsForRole(String role) throws StorageQueryException } @Override - public String[] getRolesThatHavePermission(String permission) throws StorageQueryException { + public String[] getRolesThatHavePermission(AppIdentifier appIdentifier, String permission) + throws StorageQueryException { try { + // TODO.. return UserRolesQueries.getRolesThatHavePermission(this, permission); } catch (SQLException e) { throw new StorageQueryException(e); @@ -1706,8 +1720,9 @@ public String[] getRolesThatHavePermission(String permission) throws StorageQuer } @Override - public boolean deleteRole(String role) throws StorageQueryException { + public boolean deleteRole(AppIdentifier appIdentifier, String role) throws StorageQueryException { try { + // TODO.. return UserRolesQueries.deleteRole(this, role); } catch (SQLException e) { throw new StorageQueryException(e); @@ -1715,8 +1730,9 @@ public boolean deleteRole(String role) throws StorageQueryException { } @Override - public String[] getRoles() throws StorageQueryException { + public String[] getRoles(AppIdentifier appIdentifier) throws StorageQueryException { try { + // TODO.. return UserRolesQueries.getRoles(this); } catch (SQLException e) { throw new StorageQueryException(e); @@ -1724,8 +1740,9 @@ public String[] getRoles() throws StorageQueryException { } @Override - public boolean doesRoleExist(String role) throws StorageQueryException { + public boolean doesRoleExist(AppIdentifier appIdentifier, String role) throws StorageQueryException { try { + // TODO.. return UserRolesQueries.doesRoleExist(this, role); } catch (SQLException e) { throw new StorageQueryException(e); @@ -1733,8 +1750,9 @@ public boolean doesRoleExist(String role) throws StorageQueryException { } @Override - public int deleteAllRolesForUser(String userId) throws StorageQueryException { + public int deleteAllRolesForUser(TenantIdentifier tenantIdentifier, String userId) throws StorageQueryException { try { + // TODO.. return UserRolesQueries.deleteAllRolesForUser(this, userId); } catch (SQLException e) { throw new StorageQueryException(e); @@ -1742,8 +1760,20 @@ public int deleteAllRolesForUser(String userId) throws StorageQueryException { } @Override - public boolean deleteRoleForUser_Transaction(TransactionConnection con, String userId, String role) + public void deleteAllRolesForUser(AppIdentifier appIdentifier, String userId) throws StorageQueryException { + try { + // TODO.. + UserRolesQueries.deleteAllRolesForUser(this, userId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public boolean deleteRoleForUser_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, + String userId, String role) throws StorageQueryException { + // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { @@ -1754,8 +1784,10 @@ public boolean deleteRoleForUser_Transaction(TransactionConnection con, String u } @Override - public boolean createNewRoleOrDoNothingIfExists_Transaction(TransactionConnection con, String role) + public boolean createNewRoleOrDoNothingIfExists_Transaction(TenantIdentifier tenantIdentifier, + TransactionConnection con, String role) throws StorageQueryException { + // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { @@ -1766,9 +1798,11 @@ public boolean createNewRoleOrDoNothingIfExists_Transaction(TransactionConnectio } @Override - public void addPermissionToRoleOrDoNothingIfExists_Transaction(TransactionConnection con, String role, + public void addPermissionToRoleOrDoNothingIfExists_Transaction(AppIdentifier appIdentifier, + TransactionConnection con, String role, String permission) throws StorageQueryException, UnknownRoleException { + // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { UserRolesQueries.addPermissionToRoleOrDoNothingIfExists_Transaction(this, sqlCon, role, permission); @@ -1786,8 +1820,10 @@ public void addPermissionToRoleOrDoNothingIfExists_Transaction(TransactionConnec } @Override - public boolean deletePermissionForRole_Transaction(TransactionConnection con, String role, String permission) + public boolean deletePermissionForRole_Transaction(AppIdentifier appIdentifier, TransactionConnection con, + String role, String permission) throws StorageQueryException { + // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { return UserRolesQueries.deletePermissionForRole_Transaction(this, sqlCon, role, permission); @@ -1797,9 +1833,10 @@ public boolean deletePermissionForRole_Transaction(TransactionConnection con, St } @Override - public int deleteAllPermissionsForRole_Transaction(TransactionConnection con, String role) + public int deleteAllPermissionsForRole_Transaction(AppIdentifier appIdentifier, TransactionConnection con, + String role) throws StorageQueryException { - + // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { return UserRolesQueries.deleteAllPermissionsForRole_Transaction(this, sqlCon, role); @@ -1809,7 +1846,9 @@ public int deleteAllPermissionsForRole_Transaction(TransactionConnection con, St } @Override - public boolean doesRoleExist_Transaction(TransactionConnection con, String role) throws StorageQueryException { + public boolean doesRoleExist_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String role) + throws StorageQueryException { + // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { return UserRolesQueries.doesRoleExist_transaction(this, sqlCon, role); From 39b569079018373e54eb721c3fc154175dccfdaa Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Thu, 16 Feb 2023 22:42:48 +0530 Subject: [PATCH 032/106] modifies emailpassword functions --- .../supertokens/storage/postgresql/Start.java | 28 +++++++++++++------ .../postgresql/test/ExceptionParsingTest.java | 10 +++---- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 9bcbf510..c5b4eac1 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -746,7 +746,7 @@ public void signUp(TenantIdentifier tenantIdentifier, UserInfo userInfo) } @Override - public void deleteEmailPasswordUser(TenantIdentifier tenantIdentifier, String userId) throws StorageQueryException { + public void deleteEmailPasswordUser(AppIdentifier appIdentifier, String userId) throws StorageQueryException { // TODO.. try { EmailPasswordQueries.deleteUser(this, userId); @@ -777,7 +777,7 @@ public UserInfo getUserInfoUsingEmail(TenantIdentifier tenantIdentifier, String } @Override - public void addPasswordResetToken(TenantIdentifier tenantIdentifier, PasswordResetTokenInfo passwordResetTokenInfo) + public void addPasswordResetToken(AppIdentifier appIdentifier, PasswordResetTokenInfo passwordResetTokenInfo) throws StorageQueryException, UnknownUserIdException, DuplicatePasswordResetTokenException { // TODO.. try { @@ -808,7 +808,7 @@ public void addPasswordResetToken(TenantIdentifier tenantIdentifier, PasswordRes } @Override - public PasswordResetTokenInfo getPasswordResetTokenInfo(TenantIdentifier tenantIdentifier, String token) + public PasswordResetTokenInfo getPasswordResetTokenInfo(AppIdentifier appIdentifier, String token) throws StorageQueryException { // TODO.. try { @@ -819,7 +819,7 @@ public PasswordResetTokenInfo getPasswordResetTokenInfo(TenantIdentifier tenantI } @Override - public PasswordResetTokenInfo[] getAllPasswordResetTokenInfoForUser(TenantIdentifier tenantIdentifier, + public PasswordResetTokenInfo[] getAllPasswordResetTokenInfoForUser(AppIdentifier appIdentifier, String userId) throws StorageQueryException { // TODO.. try { @@ -830,9 +830,11 @@ public PasswordResetTokenInfo[] getAllPasswordResetTokenInfoForUser(TenantIdenti } @Override - public PasswordResetTokenInfo[] getAllPasswordResetTokenInfoForUser_Transaction(TransactionConnection con, + public PasswordResetTokenInfo[] getAllPasswordResetTokenInfoForUser_Transaction(AppIdentifier appIdentifier, + TransactionConnection con, String userId) throws StorageQueryException { + // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { return EmailPasswordQueries.getAllPasswordResetTokenInfoForUser_Transaction(this, sqlCon, userId); @@ -842,8 +844,10 @@ public PasswordResetTokenInfo[] getAllPasswordResetTokenInfoForUser_Transaction( } @Override - public void deleteAllPasswordResetTokensForUser_Transaction(TransactionConnection con, String userId) + public void deleteAllPasswordResetTokensForUser_Transaction(AppIdentifier appIdentifier, TransactionConnection con, + String userId) throws StorageQueryException { + // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { EmailPasswordQueries.deleteAllPasswordResetTokensForUser_Transaction(this, sqlCon, userId); @@ -853,8 +857,10 @@ public void deleteAllPasswordResetTokensForUser_Transaction(TransactionConnectio } @Override - public void updateUsersPassword_Transaction(TransactionConnection con, String userId, String newPassword) + public void updateUsersPassword_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId, + String newPassword) throws StorageQueryException { + // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { EmailPasswordQueries.updateUsersPassword_Transaction(this, sqlCon, userId, newPassword); @@ -864,8 +870,10 @@ public void updateUsersPassword_Transaction(TransactionConnection con, String us } @Override - public void updateUsersEmail_Transaction(TransactionConnection conn, String userId, String email) + public void updateUsersEmail_Transaction(AppIdentifier appIdentifier, TransactionConnection conn, String userId, + String email) throws StorageQueryException, DuplicateEmailException { + // TODO... Connection sqlCon = (Connection) conn.getConnection(); try { EmailPasswordQueries.updateUsersEmail_Transaction(this, sqlCon, userId, email); @@ -886,8 +894,10 @@ public void updateUsersEmail_Transaction(TransactionConnection conn, String user } @Override - public UserInfo getUserInfoUsingId_Transaction(TransactionConnection con, String userId) + public UserInfo getUserInfoUsingId_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, + String userId) throws StorageQueryException { + // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { return EmailPasswordQueries.getUserInfoUsingId_Transaction(this, sqlCon, userId); diff --git a/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java b/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java index ff789af4..04ee9f15 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java @@ -177,7 +177,7 @@ public void updateUsersEmail_TransactionExceptions() storage.signUp(new TenantIdentifier(null, null, null), info2); storage.startTransaction(conn -> { try { - storage.updateUsersEmail_Transaction(conn, userId, userEmail2); + storage.updateUsersEmail_Transaction(new AppIdentifier(null, null), conn, userId, userEmail2); throw new StorageTransactionLogicException(new Exception("This should throw")); } catch (DuplicateEmailException ex) { // expected @@ -187,7 +187,7 @@ public void updateUsersEmail_TransactionExceptions() storage.startTransaction(conn -> { try { - storage.updateUsersEmail_Transaction(conn, userId, userEmail3); + storage.updateUsersEmail_Transaction(new AppIdentifier(null, null), conn, userId, userEmail3); } catch (DuplicateEmailException ex) { throw new StorageQueryException(ex); } @@ -272,13 +272,13 @@ public void addPasswordResetTokenExceptions() throws Exception { var userInfo = new UserInfo(userId, userEmail, pwHash, System.currentTimeMillis()); var info = new PasswordResetTokenInfo(userId, tokenHash, System.currentTimeMillis() + 10000); try { - storage.addPasswordResetToken(new TenantIdentifier(null, null, null), info); + storage.addPasswordResetToken(new AppIdentifier(null, null), info); } catch (UnknownUserIdException ex) { storage.signUp(new TenantIdentifier(null, null, null), userInfo); } - storage.addPasswordResetToken(new TenantIdentifier(null, null, null), info); + storage.addPasswordResetToken(new AppIdentifier(null, null), info); try { - storage.addPasswordResetToken(new TenantIdentifier(null, null, null), info); + storage.addPasswordResetToken(new AppIdentifier(null, null), info); throw new Exception("This should throw"); } catch (DuplicatePasswordResetTokenException ex) { // expected From 145a4be0fdb500bbcde5e7643a315a563e74b793 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Fri, 17 Feb 2023 12:41:13 +0530 Subject: [PATCH 033/106] changes to a few functions --- src/main/java/io/supertokens/storage/postgresql/Start.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index c5b4eac1..17575788 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -756,7 +756,7 @@ public void deleteEmailPasswordUser(AppIdentifier appIdentifier, String userId) } @Override - public UserInfo getUserInfoUsingId(TenantIdentifier tenantIdentifier, String id) throws StorageQueryException { + public UserInfo getUserInfoUsingId(AppIdentifier appIdentifier, String id) throws StorageQueryException { // TODO.. try { return EmailPasswordQueries.getUserInfoUsingId(this, id); @@ -894,7 +894,7 @@ public void updateUsersEmail_Transaction(AppIdentifier appIdentifier, Transactio } @Override - public UserInfo getUserInfoUsingId_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, + public UserInfo getUserInfoUsingId_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId) throws StorageQueryException { // TODO.. From ebd1131cb7a07a35cb4f30e205831271baf772f2 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Fri, 17 Feb 2023 14:10:59 +0530 Subject: [PATCH 034/106] adds appidentifier to email verfication --- .../supertokens/storage/postgresql/Start.java | 39 +++++++++++++------ .../postgresql/test/ExceptionParsingTest.java | 14 ++++--- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 17575788..654b1c06 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -684,7 +684,7 @@ public void addInfoToNonAuthRecipesBasedOnUserId(String className, String userId try { EmailVerificationTokenInfo info = new EmailVerificationTokenInfo(userId, "someToken", 10000, "test123@example.com"); - addEmailVerificationToken(info); + addEmailVerificationToken(new AppIdentifier(null, null), info); } catch (DuplicateEmailVerificationTokenException e) { throw new StorageQueryException(e); @@ -916,9 +916,11 @@ public void deleteExpiredEmailVerificationTokens() throws StorageQueryException } @Override - public EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUser_Transaction(TransactionConnection con, + public EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUser_Transaction(AppIdentifier appIdentifier, + TransactionConnection con, String userId, String email) throws StorageQueryException { + // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { return EmailVerificationQueries.getAllEmailVerificationTokenInfoForUser_Transaction(this, sqlCon, userId, @@ -929,8 +931,10 @@ public EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUser_Tran } @Override - public void deleteAllEmailVerificationTokensForUser_Transaction(TransactionConnection con, String userId, + public void deleteAllEmailVerificationTokensForUser_Transaction(AppIdentifier appIdentifier, + TransactionConnection con, String userId, String email) throws StorageQueryException { + // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { EmailVerificationQueries.deleteAllEmailVerificationTokensForUser_Transaction(this, sqlCon, userId, email); @@ -940,8 +944,10 @@ public void deleteAllEmailVerificationTokensForUser_Transaction(TransactionConne } @Override - public void updateIsEmailVerified_Transaction(TransactionConnection con, String userId, String email, + public void updateIsEmailVerified_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId, + String email, boolean isEmailVerified) throws StorageQueryException { + // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { EmailVerificationQueries.updateUsersIsEmailVerified_Transaction(this, sqlCon, userId, email, @@ -964,8 +970,10 @@ public void updateIsEmailVerified_Transaction(TransactionConnection con, String } @Override - public void deleteEmailVerificationUserInfo(String userId) throws StorageQueryException { + public void deleteEmailVerificationUserInfo(AppIdentifier appIdentifier, String userId) + throws StorageQueryException { try { + // TODO.. EmailVerificationQueries.deleteUserInfo(this, userId); } catch (StorageTransactionLogicException e) { throw new StorageQueryException(e.actualException); @@ -973,9 +981,10 @@ public void deleteEmailVerificationUserInfo(String userId) throws StorageQueryEx } @Override - public void addEmailVerificationToken(EmailVerificationTokenInfo emailVerificationInfo) + public void addEmailVerificationToken(AppIdentifier appIdentifier, EmailVerificationTokenInfo emailVerificationInfo) throws StorageQueryException, DuplicateEmailVerificationTokenException { try { + // TODO.. EmailVerificationQueries.addEmailVerificationToken(this, emailVerificationInfo.userId, emailVerificationInfo.token, emailVerificationInfo.tokenExpiry, emailVerificationInfo.email); } catch (SQLException e) { @@ -996,8 +1005,10 @@ public void addEmailVerificationToken(EmailVerificationTokenInfo emailVerificati } @Override - public EmailVerificationTokenInfo getEmailVerificationTokenInfo(String token) throws StorageQueryException { + public EmailVerificationTokenInfo getEmailVerificationTokenInfo(AppIdentifier appIdentifier, String token) + throws StorageQueryException { try { + // TODO.. return EmailVerificationQueries.getEmailVerificationTokenInfo(this, token); } catch (SQLException e) { throw new StorageQueryException(e); @@ -1005,8 +1016,9 @@ public EmailVerificationTokenInfo getEmailVerificationTokenInfo(String token) th } @Override - public void revokeAllTokens(String userId, String email) throws StorageQueryException { + public void revokeAllTokens(AppIdentifier appIdentifier, String userId, String email) throws StorageQueryException { try { + // TODO.. EmailVerificationQueries.revokeAllTokens(this, userId, email); } catch (SQLException e) { throw new StorageQueryException(e); @@ -1014,8 +1026,9 @@ public void revokeAllTokens(String userId, String email) throws StorageQueryExce } @Override - public void unverifyEmail(String userId, String email) throws StorageQueryException { + public void unverifyEmail(AppIdentifier appIdentifier, String userId, String email) throws StorageQueryException { try { + // TODO.. EmailVerificationQueries.unverifyEmail(this, userId, email); } catch (SQLException e) { throw new StorageQueryException(e); @@ -1023,8 +1036,10 @@ public void unverifyEmail(String userId, String email) throws StorageQueryExcept } @Override - public EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUser(String userId, String email) + public EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUser(AppIdentifier appIdentifier, + String userId, String email) throws StorageQueryException { + // TODO.. try { return EmailVerificationQueries.getAllEmailVerificationTokenInfoForUser(this, userId, email); } catch (SQLException e) { @@ -1033,8 +1048,10 @@ public EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUser(Stri } @Override - public boolean isEmailVerified(String userId, String email) throws StorageQueryException { + public boolean isEmailVerified(AppIdentifier appIdentifier, String userId, String email) + throws StorageQueryException { try { + // TODO.. return EmailVerificationQueries.isEmailVerified(this, userId, email); } catch (SQLException e) { throw new StorageQueryException(e); diff --git a/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java b/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java index 04ee9f15..b8ef9cb0 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java @@ -219,16 +219,17 @@ public void updateIsEmailVerified_TransactionExceptions() String userEmail = "useremail@asdf.fdas"; storage.startTransaction(conn -> { - storage.updateIsEmailVerified_Transaction(conn, userId, userEmail, true); + storage.updateIsEmailVerified_Transaction(new AppIdentifier(null, null), conn, userId, userEmail, true); // The insert in this call throws, but it's swallowed in the method - storage.updateIsEmailVerified_Transaction(conn, userId, userEmail, true); + storage.updateIsEmailVerified_Transaction(new AppIdentifier(null, null), conn, userId, userEmail, true); return true; }); storage.startTransaction(conn -> { try { // This call should throw, and the method shouldn't swallow it - storage.updateIsEmailVerified_Transaction(conn, null, userEmail, true); + storage.updateIsEmailVerified_Transaction(new AppIdentifier(null, null), conn, null, userEmail, + true); throw new StorageTransactionLogicException(new Exception("This should throw")); } catch (StorageQueryException ex) { // expected @@ -237,7 +238,8 @@ public void updateIsEmailVerified_TransactionExceptions() }); storage.startTransaction(conn -> { - storage.updateIsEmailVerified_Transaction(conn, userId, userEmail, false); + storage.updateIsEmailVerified_Transaction(new AppIdentifier(null, null), conn, userId, userEmail, + false); return true; }); @@ -303,9 +305,9 @@ public void addEmailVerificationTokenExceptions() throws Exception { String userEmail = "useremail@asdf.fdas"; var info = new EmailVerificationTokenInfo(userId, tokenHash, System.currentTimeMillis() + 10000, userEmail); - storage.addEmailVerificationToken(info); + storage.addEmailVerificationToken(new AppIdentifier(null, null), info); try { - storage.addEmailVerificationToken(info); + storage.addEmailVerificationToken(new AppIdentifier(null, null), info); throw new Exception("This should throw"); } catch (DuplicateEmailVerificationTokenException ex) { // expected From 437f39eb09151ccc0bc3b94bc152bf98e170d4ea Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Fri, 17 Feb 2023 14:49:23 +0530 Subject: [PATCH 035/106] makes tests pass --- .../io/supertokens/storage/postgresql/Start.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 654b1c06..93e14119 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -44,10 +44,7 @@ import io.supertokens.pluginInterface.jwt.JWTSigningKeyInfo; import io.supertokens.pluginInterface.jwt.exceptions.DuplicateKeyIdException; import io.supertokens.pluginInterface.jwt.sqlstorage.JWTRecipeSQLStorage; -import io.supertokens.pluginInterface.multitenancy.AppIdentifier; -import io.supertokens.pluginInterface.multitenancy.MultitenancyStorage; -import io.supertokens.pluginInterface.multitenancy.TenantConfig; -import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.multitenancy.*; import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateTenantException; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.passwordless.PasswordlessCode; @@ -2029,7 +2026,12 @@ public void deleteConnectionUriDomainMapping(TenantIdentifier tenantIdentifier) @Override public TenantConfig[] getAllTenants() { // TODO: - return new TenantConfig[0]; + return new TenantConfig[]{ + new TenantConfig( + new TenantIdentifier(null, null, null), + new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), new JsonObject()) + }; } @Override From 805a9a7a64f2e0f0d08c7e221b03a86bacea5250 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Fri, 17 Feb 2023 17:15:12 +0530 Subject: [PATCH 036/106] adds tenant identifier to third party --- .../supertokens/storage/postgresql/Start.java | 68 ++++++------------- .../postgresql/queries/ThirdPartyQueries.java | 58 ++-------------- .../postgresql/test/ExceptionParsingTest.java | 6 +- 3 files changed, 29 insertions(+), 103 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 93e14119..f7b91815 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -1065,12 +1065,14 @@ public void deleteExpiredPasswordResetTokens() throws StorageQueryException { } @Override - public io.supertokens.pluginInterface.thirdparty.UserInfo getUserInfoUsingId_Transaction(TransactionConnection con, - String thirdPartyId, - String thirdPartyUserId) + public io.supertokens.pluginInterface.thirdparty.UserInfo getUserInfoUsingId_Transaction( + TenantIdentifier tenantIdentifier, TransactionConnection con, + String thirdPartyId, + String thirdPartyUserId) throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { + // TODO.. return ThirdPartyQueries.getUserInfoUsingId_Transaction(this, sqlCon, thirdPartyId, thirdPartyUserId); } catch (SQLException e) { throw new StorageQueryException(e); @@ -1078,9 +1080,11 @@ public io.supertokens.pluginInterface.thirdparty.UserInfo getUserInfoUsingId_Tra } @Override - public void updateUserEmail_Transaction(TransactionConnection con, String thirdPartyId, String thirdPartyUserId, + public void updateUserEmail_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, + String thirdPartyId, String thirdPartyUserId, String newEmail) throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); + // TODO.. try { ThirdPartyQueries.updateUserEmail_Transaction(this, sqlCon, thirdPartyId, thirdPartyUserId, newEmail); } catch (SQLException e) { @@ -1089,9 +1093,10 @@ public void updateUserEmail_Transaction(TransactionConnection con, String thirdP } @Override - public void signUp(io.supertokens.pluginInterface.thirdparty.UserInfo userInfo) + public void signUp(TenantIdentifier tenantIdentifier, io.supertokens.pluginInterface.thirdparty.UserInfo userInfo) throws StorageQueryException, io.supertokens.pluginInterface.thirdparty.exception.DuplicateUserIdException, DuplicateThirdPartyUserException { + // TODO.. try { ThirdPartyQueries.signUp(this, userInfo); } catch (StorageTransactionLogicException eTemp) { @@ -1120,8 +1125,9 @@ public void signUp(io.supertokens.pluginInterface.thirdparty.UserInfo userInfo) } @Override - public void deleteThirdPartyUser(String userId) throws StorageQueryException { + public void deleteThirdPartyUser(AppIdentifier appIdentifier, String userId) throws StorageQueryException { try { + // TODO.. ThirdPartyQueries.deleteUser(this, userId); } catch (StorageTransactionLogicException e) { throw new StorageQueryException(e.actualException); @@ -1129,9 +1135,11 @@ public void deleteThirdPartyUser(String userId) throws StorageQueryException { } @Override - public io.supertokens.pluginInterface.thirdparty.UserInfo getThirdPartyUserInfoUsingId(String thirdPartyId, - String thirdPartyUserId) + public io.supertokens.pluginInterface.thirdparty.UserInfo getThirdPartyUserInfoUsingId( + TenantIdentifier tenantIdentifier, String thirdPartyId, + String thirdPartyUserId) throws StorageQueryException { + // TODO.. try { return ThirdPartyQueries.getThirdPartyUserInfoUsingId(this, thirdPartyId, thirdPartyUserId); } catch (SQLException e) { @@ -1140,8 +1148,10 @@ public io.supertokens.pluginInterface.thirdparty.UserInfo getThirdPartyUserInfoU } @Override - public io.supertokens.pluginInterface.thirdparty.UserInfo getThirdPartyUserInfoUsingId(String id) + public io.supertokens.pluginInterface.thirdparty.UserInfo getThirdPartyUserInfoUsingId(AppIdentifier appIdentifier, + String id) throws StorageQueryException { + // TODO.. try { return ThirdPartyQueries.getThirdPartyUserInfoUsingId(this, id); } catch (SQLException e) { @@ -1150,44 +1160,10 @@ public io.supertokens.pluginInterface.thirdparty.UserInfo getThirdPartyUserInfoU } @Override - @Deprecated - public io.supertokens.pluginInterface.thirdparty.UserInfo[] getThirdPartyUsers(@NotNull String userId, - @NotNull Long timeJoined, - @NotNull Integer limit, - @NotNull String timeJoinedOrder) - throws StorageQueryException { - try { - return ThirdPartyQueries.getThirdPartyUsers(this, userId, timeJoined, limit, timeJoinedOrder); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - - @Override - @Deprecated - public io.supertokens.pluginInterface.thirdparty.UserInfo[] getThirdPartyUsers(@NotNull Integer limit, - @NotNull String timeJoinedOrder) - throws StorageQueryException { - try { - return ThirdPartyQueries.getThirdPartyUsers(this, limit, timeJoinedOrder); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - - @Override - @Deprecated - public long getThirdPartyUsersCount() throws StorageQueryException { - try { - return ThirdPartyQueries.getUsersCount(this); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - - @Override - public io.supertokens.pluginInterface.thirdparty.UserInfo[] getThirdPartyUsersByEmail(@NotNull String email) + public io.supertokens.pluginInterface.thirdparty.UserInfo[] getThirdPartyUsersByEmail( + TenantIdentifier tenantIdentifier, @NotNull String email) throws StorageQueryException { + // TODO.. try { return ThirdPartyQueries.getThirdPartyUsersByEmail(this, email); } catch (SQLException e) { diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/ThirdPartyQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/ThirdPartyQueries.java index c5cf65fb..e6088af4 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/ThirdPartyQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/ThirdPartyQueries.java @@ -33,7 +33,6 @@ import java.util.List; import static io.supertokens.pluginInterface.RECIPE_ID.THIRD_PARTY; -import static io.supertokens.storage.postgresql.PreparedStatementValueSetter.NO_OP_SETTER; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.update; import static io.supertokens.storage.postgresql.config.Config.getConfig; @@ -180,7 +179,8 @@ public static UserInfo getThirdPartyUserInfoUsingId(Start start, String thirdPar } public static void updateUserEmail_Transaction(Start start, Connection con, String thirdPartyId, - String thirdPartyUserId, String newEmail) throws SQLException, StorageQueryException { + String thirdPartyUserId, String newEmail) + throws SQLException, StorageQueryException { String QUERY = "UPDATE " + getConfig(start).getThirdPartyUsersTable() + " SET email = ? WHERE third_party_id = ? AND third_party_user_id = ?"; @@ -192,7 +192,8 @@ public static void updateUserEmail_Transaction(Start start, Connection con, Stri } public static UserInfo getUserInfoUsingId_Transaction(Start start, Connection con, String thirdPartyId, - String thirdPartyUserId) throws SQLException, StorageQueryException { + String thirdPartyUserId) + throws SQLException, StorageQueryException { String QUERY = "SELECT user_id, third_party_id, third_party_user_id, email, time_joined FROM " + getConfig(start).getThirdPartyUsersTable() @@ -208,57 +209,6 @@ public static UserInfo getUserInfoUsingId_Transaction(Start start, Connection co }); } - @Deprecated - public static UserInfo[] getThirdPartyUsers(Start start, @NotNull Integer limit, @NotNull String timeJoinedOrder) - throws SQLException, StorageQueryException { - String QUERY = "SELECT user_id, third_party_id, third_party_user_id, email, time_joined FROM " - + getConfig(start).getThirdPartyUsersTable() + " ORDER BY time_joined " + timeJoinedOrder - + ", user_id DESC LIMIT ?"; - return execute(start, QUERY, pst -> pst.setInt(1, limit), result -> { - List temp = new ArrayList<>(); - while (result.next()) { - temp.add(UserInfoRowMapper.getInstance().mapOrThrow(result)); - } - return temp.toArray(UserInfo[]::new); - }); - } - - @Deprecated - public static UserInfo[] getThirdPartyUsers(Start start, @NotNull String userId, @NotNull Long timeJoined, - @NotNull Integer limit, @NotNull String timeJoinedOrder) throws SQLException, StorageQueryException { - String timeJoinedOrderSymbol = timeJoinedOrder.equals("ASC") ? ">" : "<"; - String QUERY = "SELECT user_id, third_party_id, third_party_user_id, email, time_joined FROM " - + Config.getConfig(start).getThirdPartyUsersTable() + " WHERE time_joined " + timeJoinedOrderSymbol - + " ? OR (time_joined = ? AND user_id <= ?) ORDER BY time_joined " + timeJoinedOrder - + ", user_id DESC LIMIT ?"; - - return execute(start, QUERY, pst -> { - pst.setLong(1, timeJoined); - pst.setLong(2, timeJoined); - pst.setString(3, userId); - pst.setInt(4, limit); - }, result -> { - List users = new ArrayList<>(); - - while (result.next()) { - users.add(UserInfoRowMapper.getInstance().mapOrThrow(result)); - } - - return users.toArray(UserInfo[]::new); - }); - } - - @Deprecated - public static long getUsersCount(Start start) throws SQLException, StorageQueryException { - String QUERY = "SELECT COUNT(*) as total FROM " + getConfig(start).getThirdPartyUsersTable(); - return execute(start, QUERY, NO_OP_SETTER, result -> { - if (result.next()) { - return result.getLong("total"); - } - return 0L; - }); - } - public static UserInfo[] getThirdPartyUsersByEmail(Start start, @NotNull String email) throws SQLException, StorageQueryException { String QUERY = "SELECT user_id, third_party_id, third_party_user_id, email, time_joined FROM " diff --git a/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java b/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java index b8ef9cb0..fcdae040 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java @@ -87,9 +87,9 @@ public void thirdPartySignupExceptions() throws Exception { var tp = new io.supertokens.pluginInterface.thirdparty.UserInfo.ThirdParty(tpId, thirdPartyUserId); var info = new io.supertokens.pluginInterface.thirdparty.UserInfo(userId, userEmail, tp, System.currentTimeMillis()); - storage.signUp(info); + storage.signUp(new TenantIdentifier(null, null, null), info); try { - storage.signUp(info); + storage.signUp(new TenantIdentifier(null, null, null), info); throw new Exception("This should throw"); } catch (io.supertokens.pluginInterface.thirdparty.exception.DuplicateUserIdException ex) { // expected @@ -98,7 +98,7 @@ public void thirdPartySignupExceptions() throws Exception { System.currentTimeMillis()); try { - storage.signUp(info2); + storage.signUp(new TenantIdentifier(null, null, null), info2); throw new Exception("This should throw"); } catch (DuplicateThirdPartyUserException ex) { // expected From 38d10b1b0f09a616b8df350a63ca15c33d9fe6e8 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Fri, 17 Feb 2023 18:58:08 +0530 Subject: [PATCH 037/106] adds tenantidentifier to passwordless --- .../supertokens/storage/postgresql/Start.java | 119 ++++++++++++++---- 1 file changed, 94 insertions(+), 25 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index f7b91815..27e8550e 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -1270,8 +1270,10 @@ private boolean isPrimaryKeyError(ServerErrorMessage serverMessage, String table } @Override - public PasswordlessDevice getDevice_Transaction(TransactionConnection con, String deviceIdHash) + public PasswordlessDevice getDevice_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, + String deviceIdHash) throws StorageQueryException { + // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { return PasswordlessQueries.getDevice_Transaction(this, sqlCon, deviceIdHash); @@ -1281,8 +1283,10 @@ public PasswordlessDevice getDevice_Transaction(TransactionConnection con, Strin } @Override - public void incrementDeviceFailedAttemptCount_Transaction(TransactionConnection con, String deviceIdHash) + public void incrementDeviceFailedAttemptCount_Transaction(TenantIdentifier tenantIdentifier, + TransactionConnection con, String deviceIdHash) throws StorageQueryException { + // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { PasswordlessQueries.incrementDeviceFailedAttemptCount_Transaction(this, sqlCon, deviceIdHash); @@ -1293,8 +1297,10 @@ public void incrementDeviceFailedAttemptCount_Transaction(TransactionConnection } @Override - public PasswordlessCode[] getCodesOfDevice_Transaction(TransactionConnection con, String deviceIdHash) + public PasswordlessCode[] getCodesOfDevice_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, + String deviceIdHash) throws StorageQueryException { + // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { return PasswordlessQueries.getCodesOfDevice_Transaction(this, sqlCon, deviceIdHash); @@ -1304,7 +1310,9 @@ public PasswordlessCode[] getCodesOfDevice_Transaction(TransactionConnection con } @Override - public void deleteDevice_Transaction(TransactionConnection con, String deviceIdHash) throws StorageQueryException { + public void deleteDevice_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, + String deviceIdHash) throws StorageQueryException { + // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { PasswordlessQueries.deleteDevice_Transaction(this, sqlCon, deviceIdHash); @@ -1315,8 +1323,10 @@ public void deleteDevice_Transaction(TransactionConnection con, String deviceIdH } @Override - public void deleteDevicesByPhoneNumber_Transaction(TransactionConnection con, @Nonnull String phoneNumber) + public void deleteDevicesByPhoneNumber_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, + @Nonnull String phoneNumber) throws StorageQueryException { + // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { PasswordlessQueries.deleteDevicesByPhoneNumber_Transaction(this, sqlCon, phoneNumber); @@ -1326,8 +1336,34 @@ public void deleteDevicesByPhoneNumber_Transaction(TransactionConnection con, @N } @Override - public void deleteDevicesByEmail_Transaction(TransactionConnection con, @Nonnull String email) + public void deleteDevicesByEmail_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, + @Nonnull String email) throws StorageQueryException { + // TODO.. + Connection sqlCon = (Connection) con.getConnection(); + try { + PasswordlessQueries.deleteDevicesByEmail_Transaction(this, sqlCon, email); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public void deleteDevicesByPhoneNumber_Transaction(AppIdentifier appIdentifier, TransactionConnection con, + String phoneNumber, String userId) throws StorageQueryException { + // TODO.. + Connection sqlCon = (Connection) con.getConnection(); + try { + PasswordlessQueries.deleteDevicesByPhoneNumber_Transaction(this, sqlCon, phoneNumber); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public void deleteDevicesByEmail_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String email, + String userId) throws StorageQueryException { + // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { PasswordlessQueries.deleteDevicesByEmail_Transaction(this, sqlCon, email); @@ -1337,8 +1373,10 @@ public void deleteDevicesByEmail_Transaction(TransactionConnection con, @Nonnull } @Override - public PasswordlessCode getCodeByLinkCodeHash_Transaction(TransactionConnection con, String linkCodeHash) + public PasswordlessCode getCodeByLinkCodeHash_Transaction(TenantIdentifier tenantIdentifier, + TransactionConnection con, String linkCodeHash) throws StorageQueryException { + // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { return PasswordlessQueries.getCodeByLinkCodeHash_Transaction(this, sqlCon, linkCodeHash); @@ -1348,7 +1386,9 @@ public PasswordlessCode getCodeByLinkCodeHash_Transaction(TransactionConnection } @Override - public void deleteCode_Transaction(TransactionConnection con, String deviceIdHash) throws StorageQueryException { + public void deleteCode_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, + String deviceIdHash) throws StorageQueryException { + // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { PasswordlessQueries.deleteCode_Transaction(this, sqlCon, deviceIdHash); @@ -1358,8 +1398,10 @@ public void deleteCode_Transaction(TransactionConnection con, String deviceIdHas } @Override - public void updateUserEmail_Transaction(TransactionConnection con, String userId, String email) + public void updateUserEmail_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId, + String email) throws StorageQueryException, UnknownUserIdException, DuplicateEmailException { + // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { int updated_rows = PasswordlessQueries.updateUserEmail_Transaction(this, sqlCon, userId, email); @@ -1381,8 +1423,10 @@ public void updateUserEmail_Transaction(TransactionConnection con, String userId } @Override - public void updateUserPhoneNumber_Transaction(TransactionConnection con, String userId, String phoneNumber) + public void updateUserPhoneNumber_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId, + String phoneNumber) throws StorageQueryException, UnknownUserIdException, DuplicatePhoneNumberException { + // TODO... Connection sqlCon = (Connection) con.getConnection(); try { int updated_rows = PasswordlessQueries.updateUserPhoneNumber_Transaction(this, sqlCon, userId, phoneNumber); @@ -1406,10 +1450,12 @@ public void updateUserPhoneNumber_Transaction(TransactionConnection con, String } @Override - public void createDeviceWithCode(@Nullable String email, @Nullable String phoneNumber, @NotNull String linkCodeSalt, + public void createDeviceWithCode(TenantIdentifier tenantIdentifier, @Nullable String email, + @Nullable String phoneNumber, @NotNull String linkCodeSalt, PasswordlessCode code) throws StorageQueryException, DuplicateDeviceIdHashException, DuplicateCodeIdException, DuplicateLinkCodeHashException { + // TODO.. if (email == null && phoneNumber == null) { throw new IllegalArgumentException("Both email and phoneNumber can't be null"); } @@ -1439,9 +1485,10 @@ public void createDeviceWithCode(@Nullable String email, @Nullable String phoneN } @Override - public void createCode(PasswordlessCode code) throws StorageQueryException, UnknownDeviceIdHash, + public void createCode(TenantIdentifier tenantIdentifier, PasswordlessCode code) + throws StorageQueryException, UnknownDeviceIdHash, DuplicateCodeIdException, DuplicateLinkCodeHashException { - + // TODO.. try { PasswordlessQueries.createCode(this, code); } catch (StorageTransactionLogicException e) { @@ -1468,9 +1515,11 @@ public void createCode(PasswordlessCode code) throws StorageQueryException, Unkn } @Override - public void createUser(io.supertokens.pluginInterface.passwordless.UserInfo user) throws StorageQueryException, + public void createUser(TenantIdentifier tenantIdentifier, io.supertokens.pluginInterface.passwordless.UserInfo user) + throws StorageQueryException, DuplicateEmailException, DuplicatePhoneNumberException, DuplicateUserIdException { try { + // TODO.. PasswordlessQueries.createUser(this, user); } catch (StorageTransactionLogicException e) { @@ -1501,8 +1550,9 @@ public void createUser(io.supertokens.pluginInterface.passwordless.UserInfo user } @Override - public void deletePasswordlessUser(String userId) throws StorageQueryException { + public void deletePasswordlessUser(AppIdentifier appIdentifier, String userId) throws StorageQueryException { try { + // TODO.. PasswordlessQueries.deleteUser(this, userId); } catch (StorageTransactionLogicException e) { throw new StorageQueryException(e.actualException); @@ -1510,8 +1560,10 @@ public void deletePasswordlessUser(String userId) throws StorageQueryException { } @Override - public PasswordlessDevice getDevice(String deviceIdHash) throws StorageQueryException { + public PasswordlessDevice getDevice(TenantIdentifier tenantIdentifier, String deviceIdHash) + throws StorageQueryException { try { + // TODO.. return PasswordlessQueries.getDevice(this, deviceIdHash); } catch (SQLException e) { throw new StorageQueryException(e); @@ -1519,8 +1571,10 @@ public PasswordlessDevice getDevice(String deviceIdHash) throws StorageQueryExce } @Override - public PasswordlessDevice[] getDevicesByEmail(String email) throws StorageQueryException { + public PasswordlessDevice[] getDevicesByEmail(TenantIdentifier tenantIdentifier, String email) + throws StorageQueryException { try { + // TODO.. return PasswordlessQueries.getDevicesByEmail(this, email); } catch (SQLException e) { throw new StorageQueryException(e); @@ -1528,8 +1582,10 @@ public PasswordlessDevice[] getDevicesByEmail(String email) throws StorageQueryE } @Override - public PasswordlessDevice[] getDevicesByPhoneNumber(String phoneNumber) throws StorageQueryException { + public PasswordlessDevice[] getDevicesByPhoneNumber(TenantIdentifier tenantIdentifier, String phoneNumber) + throws StorageQueryException { try { + // TODO.. return PasswordlessQueries.getDevicesByPhoneNumber(this, phoneNumber); } catch (SQLException e) { throw new StorageQueryException(e); @@ -1537,8 +1593,10 @@ public PasswordlessDevice[] getDevicesByPhoneNumber(String phoneNumber) throws S } @Override - public PasswordlessCode[] getCodesOfDevice(String deviceIdHash) throws StorageQueryException { + public PasswordlessCode[] getCodesOfDevice(TenantIdentifier tenantIdentifier, String deviceIdHash) + throws StorageQueryException { try { + // TODO.. return PasswordlessQueries.getCodesOfDevice(this, deviceIdHash); } catch (SQLException e) { throw new StorageQueryException(e); @@ -1546,8 +1604,10 @@ public PasswordlessCode[] getCodesOfDevice(String deviceIdHash) throws StorageQu } @Override - public PasswordlessCode[] getCodesBefore(long time) throws StorageQueryException { + public PasswordlessCode[] getCodesBefore(TenantIdentifier tenantIdentifier, long time) + throws StorageQueryException { try { + // TODO.. return PasswordlessQueries.getCodesBefore(this, time); } catch (SQLException e) { throw new StorageQueryException(e); @@ -1555,8 +1615,9 @@ public PasswordlessCode[] getCodesBefore(long time) throws StorageQueryException } @Override - public PasswordlessCode getCode(String codeId) throws StorageQueryException { + public PasswordlessCode getCode(TenantIdentifier tenantIdentifier, String codeId) throws StorageQueryException { try { + // TODO.. return PasswordlessQueries.getCode(this, codeId); } catch (SQLException e) { throw new StorageQueryException(e); @@ -1564,8 +1625,10 @@ public PasswordlessCode getCode(String codeId) throws StorageQueryException { } @Override - public PasswordlessCode getCodeByLinkCodeHash(String linkCodeHash) throws StorageQueryException { + public PasswordlessCode getCodeByLinkCodeHash(TenantIdentifier tenantIdentifier, String linkCodeHash) + throws StorageQueryException { try { + // TODO.. return PasswordlessQueries.getCodeByLinkCodeHash(this, linkCodeHash); } catch (SQLException e) { throw new StorageQueryException(e); @@ -1573,8 +1636,10 @@ public PasswordlessCode getCodeByLinkCodeHash(String linkCodeHash) throws Storag } @Override - public io.supertokens.pluginInterface.passwordless.UserInfo getUserById(String userId) + public io.supertokens.pluginInterface.passwordless.UserInfo getUserById(AppIdentifier appIdentifier, + String userId) throws StorageQueryException { + // TODO.. try { return PasswordlessQueries.getUserById(this, userId); } catch (SQLException e) { @@ -1583,8 +1648,10 @@ public io.supertokens.pluginInterface.passwordless.UserInfo getUserById(String u } @Override - public io.supertokens.pluginInterface.passwordless.UserInfo getUserByEmail(String email) + public io.supertokens.pluginInterface.passwordless.UserInfo getUserByEmail(TenantIdentifier tenantIdentifier, + String email) throws StorageQueryException { + // TODO.. try { return PasswordlessQueries.getUserByEmail(this, email); } catch (SQLException e) { @@ -1593,8 +1660,10 @@ public io.supertokens.pluginInterface.passwordless.UserInfo getUserByEmail(Strin } @Override - public io.supertokens.pluginInterface.passwordless.UserInfo getUserByPhoneNumber(String phoneNumber) + public io.supertokens.pluginInterface.passwordless.UserInfo getUserByPhoneNumber(TenantIdentifier tenantIdentifier, + String phoneNumber) throws StorageQueryException { + // TODO... try { return PasswordlessQueries.getUserByPhoneNumber(this, phoneNumber); } catch (SQLException e) { From 7e0c1655c110b0e035f37155d2d757f1028b38ee Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Mon, 20 Feb 2023 14:06:58 +0530 Subject: [PATCH 038/106] function name changes --- src/main/java/io/supertokens/storage/postgresql/Start.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 27e8550e..624f43a4 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -2092,12 +2092,12 @@ public void addRoleToTenant(TenantIdentifier tenantIdentifier, String role) } @Override - public void markAppIdAsDeleted(String appId) throws TenantOrAppNotFoundException { + public void deleteAppId(String appId) throws TenantOrAppNotFoundException { // TODO: } @Override - public void markConnectionUriDomainAsDeleted(String connectionUriDomain) throws TenantOrAppNotFoundException { + public void deleteConnectionUriDomain(String connectionUriDomain) throws TenantOrAppNotFoundException { // TODO: } } From 9ae2f2d748dd603fb27f09f0a620dbb7845e54ca Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Fri, 3 Mar 2023 10:37:36 +0530 Subject: [PATCH 039/106] fix: Multitenancy schema updates (#59) * fix: few schema changes and multitenancy impl * fix: handling pkey constraint * fix: pr comments * fix: pr comments * fix: pr comments * fix: pr comments * fix: typo and logical mistakes * fix: null handling and new exceptions * fix: refactored provider SQLs * fix: refactored select all * fix: fix for concurrent test * fix: cleanup * fix: cleanup and handle null boolean --- .../supertokens/storage/postgresql/Start.java | 74 ++++-- .../postgresql/config/PostgreSQLConfig.java | 31 ++- .../postgresql/queries/GeneralQueries.java | 103 +++++++- .../queries/MultitenancyQueries.java | 223 ++++++++++++++++++ .../multitenancy/TenantConfigSQLHelper.java | 104 ++++++++ .../ThirdPartyProviderClientSQLHelper.java | 136 +++++++++++ .../ThirdPartyProviderSQLHelper.java | 144 +++++++++++ .../postgresql/queries/utils/JsonUtils.java | 37 +++ .../test/multitenancy/StorageLayerTest.java | 7 + 9 files changed, 835 insertions(+), 24 deletions(-) create mode 100644 src/main/java/io/supertokens/storage/postgresql/queries/MultitenancyQueries.java create mode 100644 src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/TenantConfigSQLHelper.java create mode 100644 src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/ThirdPartyProviderClientSQLHelper.java create mode 100644 src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/ThirdPartyProviderSQLHelper.java create mode 100644 src/main/java/io/supertokens/storage/postgresql/queries/utils/JsonUtils.java diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 624f43a4..48d20940 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -45,7 +45,9 @@ import io.supertokens.pluginInterface.jwt.exceptions.DuplicateKeyIdException; import io.supertokens.pluginInterface.jwt.sqlstorage.JWTRecipeSQLStorage; import io.supertokens.pluginInterface.multitenancy.*; +import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateClientTypeException; import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateTenantException; +import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateThirdPartyIdException; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.passwordless.PasswordlessCode; import io.supertokens.pluginInterface.passwordless.PasswordlessDevice; @@ -241,6 +243,7 @@ public T startTransaction(TransactionLogic logic, TransactionIsolationLev // e.g., in case someone renamed constraints/tables boolean isDeadlockException = actualException instanceof SQLTransactionRollbackException || exceptionMessage.toLowerCase().contains("concurrent update") + || exceptionMessage.toLowerCase().contains("concurrent delete") || exceptionMessage.toLowerCase().contains("the transaction might succeed if retried") || // we have deadlock as well due to the DeadlockTest.java @@ -2033,13 +2036,44 @@ public HashMap getUserIdMappingForSuperTokensIds(AppIdentifier a } @Override - public void createTenant(TenantConfig config) throws DuplicateTenantException { - // TODO: + public void createTenant(TenantConfig tenantConfig) + throws DuplicateTenantException, StorageQueryException, DuplicateThirdPartyIdException, + DuplicateClientTypeException { + try { + MultitenancyQueries.createTenantConfig(this, tenantConfig); + } catch (StorageTransactionLogicException e) { + if (e.actualException instanceof PSQLException) { + PostgreSQLConfig config = Config.getConfig(this); + if (isPrimaryKeyError(((PSQLException) e.actualException).getServerErrorMessage(), config.getTenantConfigsTable())) { + throw new DuplicateTenantException(); + } + if (isPrimaryKeyError(((PSQLException) e.actualException).getServerErrorMessage(), config.getTenantThirdPartyProvidersTable())) { + throw new DuplicateThirdPartyIdException(); + } + if (isPrimaryKeyError(((PSQLException) e.actualException).getServerErrorMessage(), config.getTenantThirdPartyProviderClientsTable())) { + throw new DuplicateClientTypeException(); + } + } + + throw new StorageQueryException(e.actualException); + } } @Override - public void addTenantIdInUserPool(TenantIdentifier tenantIdentifier) throws DuplicateTenantException { - // TODO: + public void addTenantIdInUserPool(TenantIdentifier tenantIdentifier) + throws DuplicateTenantException, StorageQueryException { + try { + MultitenancyQueries.addTenantIdInUserPool(this, tenantIdentifier); + } catch (StorageTransactionLogicException e) { + if (e.actualException instanceof PSQLException) { + PostgreSQLConfig config = Config.getConfig(this); + if (isPrimaryKeyError(((PSQLException) e.actualException).getServerErrorMessage(), + config.getTenantsTable())) { + throw new DuplicateTenantException(); + } + } + throw new StorageQueryException(e.actualException); + } } @Override @@ -2048,8 +2082,26 @@ public void deleteTenantIdInUserPool(TenantIdentifier tenantIdentifier) throws T } @Override - public void overwriteTenantConfig(TenantConfig config) throws TenantOrAppNotFoundException { - // TODO: + public void overwriteTenantConfig(TenantConfig tenantConfig) + throws TenantOrAppNotFoundException, StorageQueryException, DuplicateThirdPartyIdException, + DuplicateClientTypeException { + try { + MultitenancyQueries.overwriteTenantConfig(this, tenantConfig); + } catch (StorageTransactionLogicException e) { + if (e.actualException instanceof TenantOrAppNotFoundException) { + throw (TenantOrAppNotFoundException) e.actualException; + } + if (e.actualException instanceof PSQLException) { + PostgreSQLConfig config = Config.getConfig(this); + if (isPrimaryKeyError(((PSQLException) e.actualException).getServerErrorMessage(), config.getTenantThirdPartyProvidersTable())) { + throw new DuplicateThirdPartyIdException(); + } + if (isPrimaryKeyError(((PSQLException) e.actualException).getServerErrorMessage(), config.getTenantThirdPartyProviderClientsTable())) { + throw new DuplicateClientTypeException(); + } + } + throw new StorageQueryException(e.actualException); + } } @Override @@ -2069,14 +2121,8 @@ public void deleteConnectionUriDomainMapping(TenantIdentifier tenantIdentifier) } @Override - public TenantConfig[] getAllTenants() { - // TODO: - return new TenantConfig[]{ - new TenantConfig( - new TenantIdentifier(null, null, null), - new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), - new PasswordlessConfig(true), new JsonObject()) - }; + public TenantConfig[] getAllTenants() throws StorageQueryException { + return MultitenancyQueries.getAllTenants(this); } @Override diff --git a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java index b3d6ca74..feb4dd8a 100644 --- a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java +++ b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java @@ -215,6 +215,32 @@ public String getUsersTable() { return addSchemaAndPrefixToTableName("all_auth_recipe_users"); } + public String getAppsTable() { + String tableName = "apps"; + return addSchemaAndPrefixToTableName(tableName); + } + + public String getTenantsTable() { + String tableName = "tenants"; + return addSchemaAndPrefixToTableName(tableName); + } + + public String getTenantConfigsTable() { + String tableName = "tenant_configs"; + return addSchemaAndPrefixToTableName(tableName); + } + + public String getTenantThirdPartyProvidersTable() { + String tableName = "tenant_thirdparty_providers"; + return addSchemaAndPrefixToTableName(tableName); + } + + public String getTenantThirdPartyProviderClientsTable() { + String tableName = "tenant_thirdparty_provider_clients"; + return addSchemaAndPrefixToTableName(tableName); + } + + public String getKeyValueTable() { String tableName = "key_value"; if (postgresql_key_value_table_name != null) { @@ -223,6 +249,10 @@ public String getKeyValueTable() { return addSchemaAndPrefixToTableName(tableName); } + public String getAppIdToUserIdTable() { + return addSchemaAndPrefixToTableName("app_id_to_user_id"); + } + public String getAccessTokenSigningKeysTable() { return addSchemaAndPrefixToTableName("session_access_token_signing_keys"); } @@ -409,5 +439,4 @@ void assertThatConfigFromSameUserPoolIsNotConflicting(PostgreSQLConfig otherConf " for the same user pool"); } } - } \ No newline at end of file diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java index c06f21c9..10aa1c84 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java @@ -89,6 +89,34 @@ static String getQueryToCreateUserPaginationIndex(Start start) { + "(time_joined DESC, user_id " + "DESC);"; } + private static String getQueryToCreateAppsTable(Start start) { + String schema = Config.getConfig(start).getTableSchema(); + String appsTable = Config.getConfig(start).getAppsTable(); + // @formatter:off + return "CREATE TABLE IF NOT EXISTS " + appsTable + " (" + + "app_id VARCHAR(64) NOT NULL DEFAULT 'public'," + + "created_at_time BIGINT ," + + "CONSTRAINT " + Utils.getConstraintName(schema, appsTable, null, "pkey") + " PRIMARY KEY(app_id)" + + " );"; + // @formatter:on + } + + private static String getQueryToCreateTenantsTable(Start start) { + String schema = Config.getConfig(start).getTableSchema(); + String tenantsTable = Config.getConfig(start).getTenantsTable(); + // @formatter:off + return "CREATE TABLE IF NOT EXISTS " + tenantsTable + " (" + + "app_id VARCHAR(64) NOT NULL DEFAULT 'public'," + + "tenant_id VARCHAR(64) NOT NULL DEFAULT 'public'," + + "created_at_time BIGINT ," + + "CONSTRAINT " + Utils.getConstraintName(schema, tenantsTable, null, "pkey") + " PRIMARY KEY(app_id, tenant_id) ," + + "CONSTRAINT " + Utils.getConstraintName(schema, tenantsTable, "app_id", "fkey") + + " FOREIGN KEY(app_id)" + + " REFERENCES " + Config.getConfig(start).getAppsTable() + " (app_id) ON DELETE CASCADE" + + ");"; + // @formatter:on + } + private static String getQueryToCreateKeyValueTable(Start start) { String schema = Config.getConfig(start).getTableSchema(); String keyValueTable = Config.getConfig(start).getKeyValueTable(); @@ -102,12 +130,37 @@ private static String getQueryToCreateKeyValueTable(Start start) { // @formatter:on } + private static String getQueryToCreateAppIdToUserIdTable(Start start) { + String schema = Config.getConfig(start).getTableSchema(); + String appToUserTable = Config.getConfig(start).getAppIdToUserIdTable(); + // @formatter:off + return "CREATE TABLE IF NOT EXISTS " + appToUserTable + " (" + + "app_id VARCHAR(64) NOT NULL DEFAULT 'public'," + + "user_id CHAR(36) NOT NULL," + + "recipe_id VARCHAR(128) NOT NULL," + + "time_joined BIGINT NOT NULL," + + "CONSTRAINT " + Utils.getConstraintName(schema, appToUserTable, null, "pkey") + " PRIMARY KEY (app_id, user_id), " + + "CONSTRAINT " + Utils.getConstraintName(schema, appToUserTable, "app_id", "fkey") + " FOREIGN KEY(app_id) REFERENCES " + Config.getConfig(start).getAppsTable() + "(app_id) ON DELETE CASCADE" + + ");"; + // @formatter:on + } + public static void createTablesIfNotExists(Start start) throws SQLException, StorageQueryException { int numberOfRetries = 0; boolean retry = true; while (retry) { retry = false; try { + if (!doesTableExists(start, Config.getConfig(start).getAppsTable())) { + getInstance(start).addState(CREATING_NEW_TABLE, null); + update(start, getQueryToCreateAppsTable(start), NO_OP_SETTER); + } + + if (!doesTableExists(start, Config.getConfig(start).getTenantsTable())) { + getInstance(start).addState(CREATING_NEW_TABLE, null); + update(start, getQueryToCreateTenantsTable(start), NO_OP_SETTER); + } + if (!doesTableExists(start, Config.getConfig(start).getKeyValueTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); update(start, getQueryToCreateKeyValueTable(start), NO_OP_SETTER); @@ -121,6 +174,11 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto update(start, getQueryToCreateUserPaginationIndex(start), NO_OP_SETTER); } + if (!doesTableExists(start, Config.getConfig(start).getAppIdToUserIdTable())) { + getInstance(start).addState(CREATING_NEW_TABLE, null); + update(start, getQueryToCreateAppIdToUserIdTable(start), NO_OP_SETTER); + } + if (!doesTableExists(start, Config.getConfig(start).getAccessTokenSigningKeysTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); update(start, getQueryToCreateAccessTokenSigningKeysTable(start), NO_OP_SETTER); @@ -131,6 +189,21 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto update(start, getQueryToCreateSessionInfoTable(start), NO_OP_SETTER); } + if (!doesTableExists(start, Config.getConfig(start).getTenantConfigsTable())) { + getInstance(start).addState(CREATING_NEW_TABLE, null); + update(start, MultitenancyQueries.getQueryToCreateTenantConfigsTable(start), NO_OP_SETTER); + } + + if (!doesTableExists(start, Config.getConfig(start).getTenantThirdPartyProvidersTable())) { + getInstance(start).addState(CREATING_NEW_TABLE, null); + update(start, MultitenancyQueries.getQueryToCreateTenantThirdPartyProvidersTable(start), NO_OP_SETTER); + } + + if (!doesTableExists(start, Config.getConfig(start).getTenantThirdPartyProviderClientsTable())) { + getInstance(start).addState(CREATING_NEW_TABLE, null); + update(start, MultitenancyQueries.getQueryToCreateTenantThirdPartyProviderClientsTable(start), NO_OP_SETTER); + } + if (!doesTableExists(start, Config.getConfig(start).getEmailPasswordUsersTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); update(start, EmailPasswordQueries.getQueryToCreateUsersTable(start), NO_OP_SETTER); @@ -253,18 +326,30 @@ public static void deleteAllTables(Start start) throws SQLException, StorageQuer } { - String DROP_QUERY = "DROP TABLE IF EXISTS " + getConfig(start).getKeyValueTable() + "," - + getConfig(start).getUserIdMappingTable() + "," + getConfig(start).getUsersTable() + "," - + getConfig(start).getAccessTokenSigningKeysTable() + "," + getConfig(start).getSessionInfoTable() - + "," + getConfig(start).getEmailPasswordUsersTable() + "," + String DROP_QUERY = "DROP TABLE IF EXISTS " + + getConfig(start).getAppsTable() + "," + + getConfig(start).getTenantsTable() + "," + + getConfig(start).getKeyValueTable() + "," + + getConfig(start).getAppIdToUserIdTable() + "," + + getConfig(start).getUserIdMappingTable() + "," + + getConfig(start).getUsersTable() + "," + + getConfig(start).getAccessTokenSigningKeysTable() + "," + + getConfig(start).getTenantConfigsTable() + "," + + getConfig(start).getTenantThirdPartyProvidersTable() + "," + + getConfig(start).getTenantThirdPartyProviderClientsTable() + "," + + getConfig(start).getSessionInfoTable() + "," + + getConfig(start).getEmailPasswordUsersTable() + "," + getConfig(start).getPasswordResetTokensTable() + "," + getConfig(start).getEmailVerificationTokensTable() + "," - + getConfig(start).getEmailVerificationTable() + "," + getConfig(start).getThirdPartyUsersTable() - + "," + getConfig(start).getJWTSigningKeysTable() + "," + + getConfig(start).getEmailVerificationTable() + "," + + getConfig(start).getThirdPartyUsersTable() + "," + + getConfig(start).getJWTSigningKeysTable() + "," + getConfig(start).getPasswordlessCodesTable() + "," + getConfig(start).getPasswordlessDevicesTable() + "," - + getConfig(start).getPasswordlessUsersTable() + "," + getConfig(start).getUserMetadataTable() + "," - + getConfig(start).getRolesTable() + "," + getConfig(start).getUserRolesPermissionsTable() + "," + + getConfig(start).getPasswordlessUsersTable() + "," + + getConfig(start).getUserMetadataTable() + "," + + getConfig(start).getRolesTable() + "," + + getConfig(start).getUserRolesPermissionsTable() + "," + getConfig(start).getUserRolesTable(); update(start, DROP_QUERY, NO_OP_SETTER); } @@ -272,7 +357,7 @@ public static void deleteAllTables(Start start) throws SQLException, StorageQuer public static void setKeyValue_Transaction(Start start, Connection con, String key, KeyValueInfo info) throws SQLException, StorageQueryException { - String QUERY = "INSERT INTO " + getConfig(start).getKeyValueTable() + String QUERY = "INSERT INTO " + getConfig(start).getKeyValueTable() + "(name, value, created_at_time) VALUES(?, ?, ?) " + "ON CONFLICT (name) DO UPDATE SET value = ?, created_at_time = ?"; diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/MultitenancyQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/MultitenancyQueries.java new file mode 100644 index 00000000..c72cd645 --- /dev/null +++ b/src/main/java/io/supertokens/storage/postgresql/queries/MultitenancyQueries.java @@ -0,0 +1,223 @@ +/* + * 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.storage.postgresql.queries; + +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; +import io.supertokens.pluginInterface.multitenancy.*; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.storage.postgresql.Start; +import io.supertokens.storage.postgresql.config.Config; +import io.supertokens.storage.postgresql.queries.multitenancy.TenantConfigSQLHelper; +import io.supertokens.storage.postgresql.queries.multitenancy.ThirdPartyProviderClientSQLHelper; +import io.supertokens.storage.postgresql.queries.multitenancy.ThirdPartyProviderSQLHelper; +import io.supertokens.storage.postgresql.utils.Utils; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.HashMap; + +import static io.supertokens.storage.postgresql.QueryExecutorTemplate.update; +import static io.supertokens.storage.postgresql.config.Config.getConfig; + +public class MultitenancyQueries { + static String getQueryToCreateTenantConfigsTable(Start start) { + String schema = Config.getConfig(start).getTableSchema(); + String tenantConfigsTable = Config.getConfig(start).getTenantConfigsTable(); + // @formatter:off + return "CREATE TABLE IF NOT EXISTS " + tenantConfigsTable + " (" + + "connection_uri_domain VARCHAR(256) DEFAULT ''," + + "app_id VARCHAR(64) DEFAULT 'public'," + + "tenant_id VARCHAR(64) DEFAULT 'public'," + + "core_config TEXT," + + "email_password_enabled BOOLEAN," + + "passwordless_enabled BOOLEAN," + + "third_party_enabled BOOLEAN," + + "CONSTRAINT " + Utils.getConstraintName(schema, tenantConfigsTable, null, "pkey") + " PRIMARY KEY (connection_uri_domain, app_id, tenant_id)" + + ");"; + // @formatter:on + } + + static String getQueryToCreateTenantThirdPartyProvidersTable(Start start) { + String schema = Config.getConfig(start).getTableSchema(); + String tenantThirdPartyProvidersTable = Config.getConfig(start).getTenantThirdPartyProvidersTable(); + // @formatter:off + return "CREATE TABLE IF NOT EXISTS " + tenantThirdPartyProvidersTable + " (" + + "connection_uri_domain VARCHAR(256) DEFAULT ''," + + "app_id VARCHAR(64) DEFAULT 'public'," + + "tenant_id VARCHAR(64) DEFAULT 'public'," + + "third_party_id VARCHAR(28) NOT NULL," + + "name VARCHAR(64)," + + "authorization_endpoint TEXT," + + "authorization_endpoint_query_params TEXT," + + "token_endpoint TEXT," + + "token_endpoint_body_params TEXT," + + "user_info_endpoint TEXT," + + "user_info_endpoint_query_params TEXT," + + "user_info_endpoint_headers TEXT," + + "jwks_uri TEXT," + + "oidc_discovery_endpoint TEXT," + + "require_email BOOLEAN," + + "user_info_map_from_id_token_payload_user_id VARCHAR(64)," + + "user_info_map_from_id_token_payload_email VARCHAR(64)," + + "user_info_map_from_id_token_payload_email_verified VARCHAR(64)," + + "user_info_map_from_user_info_endpoint_user_id VARCHAR(64)," + + "user_info_map_from_user_info_endpoint_email VARCHAR(64)," + + "user_info_map_from_user_info_endpoint_email_verified VARCHAR(64)," + + "CONSTRAINT " + Utils.getConstraintName(schema, tenantThirdPartyProvidersTable, null, "pkey") + " PRIMARY KEY (connection_uri_domain, app_id, tenant_id, third_party_id)," + + "CONSTRAINT " + Utils.getConstraintName(schema, tenantThirdPartyProvidersTable, "tenant_id", "fkey") + + " FOREIGN KEY(connection_uri_domain, app_id, tenant_id)" + + " REFERENCES " + Config.getConfig(start).getTenantConfigsTable() + " (connection_uri_domain, app_id, tenant_id) ON DELETE CASCADE" + + ");"; + // @formatter:on + } + + static String getQueryToCreateTenantThirdPartyProviderClientsTable(Start start) { + String schema = Config.getConfig(start).getTableSchema(); + String tenantThirdPartyProvidersTable = Config.getConfig(start).getTenantThirdPartyProviderClientsTable(); + return "CREATE TABLE IF NOT EXISTS " + tenantThirdPartyProvidersTable + " (" + + "connection_uri_domain VARCHAR(256) DEFAULT ''," + + "app_id VARCHAR(64) DEFAULT 'public'," + + "tenant_id VARCHAR(64) DEFAULT 'public'," + + "third_party_id VARCHAR(28) NOT NULL," + + "client_type VARCHAR(64) NOT NULL DEFAULT ''," + + "client_id VARCHAR(256) NOT NULL," + + "client_secret TEXT," + + "scope VARCHAR(128)[]," + + "force_pkce BOOLEAN," + + "additional_config TEXT," + + "CONSTRAINT " + Utils.getConstraintName(schema, tenantThirdPartyProvidersTable, null, "pkey") + " PRIMARY KEY (connection_uri_domain, app_id, tenant_id, third_party_id, client_type)," + + "CONSTRAINT " + Utils.getConstraintName(schema, tenantThirdPartyProvidersTable, "third_party_id", "fkey") + + " FOREIGN KEY(connection_uri_domain, app_id, tenant_id, third_party_id)" + + " REFERENCES " + Config.getConfig(start).getTenantThirdPartyProvidersTable() + " (connection_uri_domain, app_id, tenant_id, third_party_id) ON DELETE CASCADE" + + ");"; + } + + private static void executeCreateTenantQueries(Start start, Connection sqlCon, TenantConfig tenantConfig) + throws SQLException, StorageQueryException { + + TenantConfigSQLHelper.create(start, sqlCon, tenantConfig); + + for (ThirdPartyConfig.Provider provider : tenantConfig.thirdPartyConfig.providers) { + ThirdPartyProviderSQLHelper.create(start, sqlCon, tenantConfig, provider); + + for (ThirdPartyConfig.ProviderClient providerClient : provider.clients) { + ThirdPartyProviderClientSQLHelper.create(start, sqlCon, tenantConfig, provider, providerClient); + } + } + } + + public static void createTenantConfig(Start start, TenantConfig tenantConfig) throws StorageQueryException, StorageTransactionLogicException { + start.startTransaction(con -> { + Connection sqlCon = (Connection) con.getConnection(); + { + try { + executeCreateTenantQueries(start, sqlCon, tenantConfig); + sqlCon.commit(); + } catch (SQLException throwables) { + throw new StorageTransactionLogicException(throwables); + } + } + + return null; + }); + } + + public static void overwriteTenantConfig(Start start, TenantConfig tenantConfig) throws StorageQueryException, StorageTransactionLogicException { + start.startTransaction(con -> { + Connection sqlCon = (Connection) con.getConnection(); + { + try { + { + String QUERY = "DELETE FROM " + getConfig(start).getTenantConfigsTable() + + " WHERE connection_uri_domain = ? AND app_id = ? AND tenant_id = ?;"; + int rowsAffected = update(sqlCon, QUERY, pst -> { + pst.setString(1, tenantConfig.tenantIdentifier.getConnectionUriDomain()); + pst.setString(2, tenantConfig.tenantIdentifier.getAppId()); + pst.setString(3, tenantConfig.tenantIdentifier.getTenantId()); + }); + if (rowsAffected == 0) { + throw new StorageTransactionLogicException(new TenantOrAppNotFoundException(tenantConfig.tenantIdentifier)); + } + } + + { + executeCreateTenantQueries(start, sqlCon, tenantConfig); + } + + sqlCon.commit(); + + } catch (SQLException throwables) { + throw new StorageTransactionLogicException(throwables); + } + } + + return null; + }); + } + + public static TenantConfig[] getAllTenants(Start start) throws StorageQueryException { + try { + + // Map TenantIdentifier -> thirdPartyId -> clientType + HashMap>> providerClientsMap = ThirdPartyProviderClientSQLHelper.selectAll(start); + + // Map (tenantIdentifier) -> thirdPartyId -> provider + HashMap> providerMap = ThirdPartyProviderSQLHelper.selectAll(start, providerClientsMap); + + return TenantConfigSQLHelper.selectAll(start, providerMap); + } catch (SQLException throwables) { + throw new StorageQueryException(throwables); + } + } + + public static void addTenantIdInUserPool(Start start, TenantIdentifier tenantIdentifier) throws + StorageTransactionLogicException, StorageQueryException { + { + start.startTransaction(con -> { + Connection sqlCon = (Connection) con.getConnection(); + long currentTime = System.currentTimeMillis(); + try { + { + String QUERY = "INSERT INTO " + getConfig(start).getAppsTable() + + "(app_id, created_at_time)" + " VALUES(?, ?) ON CONFLICT DO NOTHING"; + update(sqlCon, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setLong(2, currentTime); + }); + } + + { + String QUERY = "INSERT INTO " + getConfig(start).getTenantsTable() + + "(app_id, tenant_id, created_at_time)" + " VALUES(?, ?, ?)"; + + update(sqlCon, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setLong(3, currentTime); + }); + } + + sqlCon.commit(); + } catch (SQLException throwables) { + throw new StorageTransactionLogicException(throwables); + } + return null; + }); + } + } +} diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/TenantConfigSQLHelper.java b/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/TenantConfigSQLHelper.java new file mode 100644 index 00000000..0dfadb9b --- /dev/null +++ b/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/TenantConfigSQLHelper.java @@ -0,0 +1,104 @@ +/* + * 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.storage.postgresql.queries.multitenancy; + +import io.supertokens.pluginInterface.RowMapper; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.*; +import io.supertokens.storage.postgresql.Start; +import io.supertokens.storage.postgresql.queries.utils.JsonUtils; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; +import static io.supertokens.storage.postgresql.QueryExecutorTemplate.update; +import static io.supertokens.storage.postgresql.config.Config.getConfig; + +public class TenantConfigSQLHelper { + public static class TenantConfigRowMapper implements RowMapper { + ThirdPartyConfig.Provider[] providers; + + private TenantConfigRowMapper(ThirdPartyConfig.Provider[] providers) { + this.providers = providers; + } + + public static TenantConfigSQLHelper.TenantConfigRowMapper getInstance(ThirdPartyConfig.Provider[] providers) { + return new TenantConfigSQLHelper.TenantConfigRowMapper(providers); + } + + @Override + public TenantConfig map(ResultSet result) throws StorageQueryException { + try { + return new TenantConfig( + new TenantIdentifier(result.getString("connection_uri_domain"), result.getString("app_id"), result.getString("tenant_id")), + new EmailPasswordConfig(result.getBoolean("email_password_enabled")), + new ThirdPartyConfig(result.getBoolean("third_party_enabled"), this.providers), + new PasswordlessConfig(result.getBoolean("passwordless_enabled")), + JsonUtils.stringToJsonObject(result.getString("core_config")) + ); + } catch (Exception e) { + throw new StorageQueryException(e); + } + } + } + + public static TenantConfig[] selectAll(Start start, HashMap> providerMap) + throws SQLException, StorageQueryException { + String QUERY = "SELECT connection_uri_domain, app_id, tenant_id, core_config, email_password_enabled, passwordless_enabled, third_party_enabled FROM " + + getConfig(start).getTenantConfigsTable() + ";"; + + TenantConfig[] tenantConfigs = execute(start, QUERY, pst -> {}, result -> { + List temp = new ArrayList<>(); + while (result.next()) { + TenantIdentifier tenantIdentifier = new TenantIdentifier(result.getString("connection_uri_domain"), result.getString("app_id"), result.getString("tenant_id")); + ThirdPartyConfig.Provider[] providers = null; + if (providerMap.containsKey(tenantIdentifier)) { + providers = providerMap.get(tenantIdentifier).values().toArray(new ThirdPartyConfig.Provider[0]); + } + temp.add(TenantConfigSQLHelper.TenantConfigRowMapper.getInstance(providers).mapOrThrow(result)); + } + TenantConfig[] finalResult = new TenantConfig[temp.size()]; + for (int i = 0; i < temp.size(); i++) { + finalResult[i] = temp.get(i); + } + return finalResult; + }); + return tenantConfigs; + } + + public static void create(Start start, Connection sqlCon, TenantConfig tenantConfig) + throws SQLException, StorageQueryException { + String QUERY = "INSERT INTO " + getConfig(start).getTenantConfigsTable() + + "(connection_uri_domain, app_id, tenant_id, core_config, email_password_enabled, passwordless_enabled, third_party_enabled)" + " VALUES(?, ?, ?, ?, ?, ?, ?)"; + + update(sqlCon, QUERY, pst -> { + pst.setString(1, tenantConfig.tenantIdentifier.getConnectionUriDomain()); + pst.setString(2, tenantConfig.tenantIdentifier.getAppId()); + pst.setString(3, tenantConfig.tenantIdentifier.getTenantId()); + pst.setString(4, tenantConfig.coreConfig.toString()); + pst.setBoolean(5, tenantConfig.emailPasswordConfig.enabled); + pst.setBoolean(6, tenantConfig.passwordlessConfig.enabled); + pst.setBoolean(7, tenantConfig.thirdPartyConfig.enabled); + }); + } + +} diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/ThirdPartyProviderClientSQLHelper.java b/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/ThirdPartyProviderClientSQLHelper.java new file mode 100644 index 00000000..ced73d6e --- /dev/null +++ b/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/ThirdPartyProviderClientSQLHelper.java @@ -0,0 +1,136 @@ +/* + * 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.storage.postgresql.queries.multitenancy; + +import io.supertokens.pluginInterface.RowMapper; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.TenantConfig; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.multitenancy.ThirdPartyConfig; +import io.supertokens.storage.postgresql.Start; +import io.supertokens.storage.postgresql.queries.utils.JsonUtils; + +import java.sql.*; +import java.util.HashMap; +import java.util.Objects; + +import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; +import static io.supertokens.storage.postgresql.QueryExecutorTemplate.update; +import static io.supertokens.storage.postgresql.config.Config.getConfig; + +public class ThirdPartyProviderClientSQLHelper { + public static class TenantThirdPartyProviderClientRowMapper implements + RowMapper { + public static final ThirdPartyProviderClientSQLHelper.TenantThirdPartyProviderClientRowMapper INSTANCE = new ThirdPartyProviderClientSQLHelper.TenantThirdPartyProviderClientRowMapper(); + + private TenantThirdPartyProviderClientRowMapper() { + } + + public static ThirdPartyProviderClientSQLHelper.TenantThirdPartyProviderClientRowMapper getInstance() { + return INSTANCE; + } + + @Override + public ThirdPartyConfig.ProviderClient map(ResultSet result) throws StorageQueryException { + try { + Array scopeArray = result.getArray("scope"); + String[] scopeStringArray; + if (scopeArray == null) { + scopeStringArray = null; + } else { + scopeStringArray = (String[]) scopeArray.getArray(); + scopeArray.free(); + } + String clientType = result.getString("client_type"); + if (clientType.equals("")) { + clientType = null; + } + + Boolean forcePkce = result.getBoolean("force_pkce"); + if (result.wasNull()) forcePkce = null; + + return new ThirdPartyConfig.ProviderClient( + clientType, + result.getString("client_id"), + result.getString("client_secret"), + scopeStringArray, + forcePkce, + JsonUtils.stringToJsonObject(result.getString("additional_config")) + ); + } catch (Exception e) { + throw new StorageQueryException(e); + } + } + } + + public static HashMap>> selectAll(Start start) + throws SQLException, StorageQueryException { + HashMap>> providerClientsMap = new HashMap<>(); + + String QUERY = "SELECT connection_uri_domain, app_id, tenant_id, third_party_id, client_type, client_id, client_secret, scope, force_pkce, additional_config FROM " + + getConfig(start).getTenantThirdPartyProviderClientsTable() + ";"; + + execute(start, QUERY, pst -> {}, result -> { + while (result.next()) { + TenantIdentifier tenantIdentifier = new TenantIdentifier(result.getString("connection_uri_domain"), result.getString("app_id"), result.getString("tenant_id")); + ThirdPartyConfig.ProviderClient providerClient = ThirdPartyProviderClientSQLHelper.TenantThirdPartyProviderClientRowMapper.getInstance().mapOrThrow(result); + if (!providerClientsMap.containsKey(tenantIdentifier)) { + providerClientsMap.put(tenantIdentifier, new HashMap<>()); + } + + if(!providerClientsMap.get(tenantIdentifier).containsKey(result.getString("third_party_id"))) { + providerClientsMap.get(tenantIdentifier).put(result.getString("third_party_id"), new HashMap<>()); + } + + providerClientsMap.get(tenantIdentifier).get(result.getString("third_party_id")).put(providerClient.clientType, providerClient); + } + return null; + }); + return providerClientsMap; + } + + public static void create(Start start, Connection sqlCon, TenantConfig tenantConfig, ThirdPartyConfig.Provider provider, ThirdPartyConfig.ProviderClient providerClient) + throws SQLException, StorageQueryException { + + String QUERY = "INSERT INTO " + getConfig(start).getTenantThirdPartyProviderClientsTable() + + "(connection_uri_domain, app_id, tenant_id, third_party_id, client_type, client_id, client_secret, scope, force_pkce, additional_config)" + + " VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + + Array scopeArray; + if (providerClient.scope != null) { + scopeArray = sqlCon.createArrayOf("VARCHAR", providerClient.scope); + } else { + scopeArray = null; + } + update(sqlCon, QUERY, pst -> { + pst.setString(1, tenantConfig.tenantIdentifier.getConnectionUriDomain()); + pst.setString(2, tenantConfig.tenantIdentifier.getAppId()); + pst.setString(3, tenantConfig.tenantIdentifier.getTenantId()); + pst.setString(4, provider.thirdPartyId); + pst.setString(5, Objects.requireNonNullElse(providerClient.clientType, "")); + pst.setString(6, providerClient.clientId); + pst.setString(7, providerClient.clientSecret); + pst.setArray(8, scopeArray); + if (providerClient.forcePKCE == null) { + pst.setNull(9, Types.BOOLEAN); + } else { + pst.setBoolean(9, providerClient.forcePKCE.booleanValue()); + } + pst.setString(10, JsonUtils.jsonObjectToString(providerClient.additionalConfig)); + }); + } +} diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/ThirdPartyProviderSQLHelper.java b/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/ThirdPartyProviderSQLHelper.java new file mode 100644 index 00000000..3f2b6645 --- /dev/null +++ b/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/ThirdPartyProviderSQLHelper.java @@ -0,0 +1,144 @@ +/* + * 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.storage.postgresql.queries.multitenancy; + +import io.supertokens.pluginInterface.RowMapper; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.TenantConfig; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.multitenancy.ThirdPartyConfig; +import io.supertokens.storage.postgresql.Start; +import io.supertokens.storage.postgresql.queries.utils.JsonUtils; + +import java.sql.*; +import java.util.HashMap; + +import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; +import static io.supertokens.storage.postgresql.QueryExecutorTemplate.update; +import static io.supertokens.storage.postgresql.config.Config.getConfig; + +public class ThirdPartyProviderSQLHelper { + public static class TenantThirdPartyProviderRowMapper implements + RowMapper { + final private ThirdPartyConfig.ProviderClient[] clients; + + private TenantThirdPartyProviderRowMapper(ThirdPartyConfig.ProviderClient[] clients) { + this.clients = clients; + } + + public static ThirdPartyProviderSQLHelper.TenantThirdPartyProviderRowMapper getInstance(ThirdPartyConfig.ProviderClient[] clients) { + return new ThirdPartyProviderSQLHelper.TenantThirdPartyProviderRowMapper(clients); + } + + @Override + public ThirdPartyConfig.Provider map(ResultSet result) throws StorageQueryException { + try { + Boolean requireEmail = result.getBoolean("require_email"); + if (result.wasNull()) requireEmail = null; + return new ThirdPartyConfig.Provider( + result.getString("third_party_id"), + result.getString("name"), + this.clients, + result.getString("authorization_endpoint"), + JsonUtils.stringToJsonObject(result.getString("authorization_endpoint_query_params")), + result.getString("token_endpoint"), + JsonUtils.stringToJsonObject(result.getString("token_endpoint_body_params")), + result.getString("user_info_endpoint"), + JsonUtils.stringToJsonObject(result.getString("user_info_endpoint_query_params")), + JsonUtils.stringToJsonObject(result.getString("user_info_endpoint_headers")), + result.getString("jwks_uri"), + result.getString("oidc_discovery_endpoint"), + requireEmail, + new ThirdPartyConfig.UserInfoMap( + new ThirdPartyConfig.UserInfoMapKeyValue( + result.getString("user_info_map_from_id_token_payload_user_id"), + result.getString("user_info_map_from_id_token_payload_email"), + result.getString("user_info_map_from_id_token_payload_email_verified") + ), + new ThirdPartyConfig.UserInfoMapKeyValue( + result.getString("user_info_map_from_user_info_endpoint_user_id"), + result.getString("user_info_map_from_user_info_endpoint_email"), + result.getString("user_info_map_from_user_info_endpoint_email_verified") + ) + ) + ); + } catch (Exception e) { + throw new StorageQueryException(e); + } + } + } + + public static HashMap> selectAll(Start start, HashMap>> providerClientsMap) + throws SQLException, StorageQueryException { + HashMap> providerMap = new HashMap<>(); + + String QUERY = "SELECT connection_uri_domain, app_id, tenant_id, third_party_id, name, authorization_endpoint, authorization_endpoint_query_params, token_endpoint, token_endpoint_body_params, user_info_endpoint, user_info_endpoint_query_params, user_info_endpoint_headers, jwks_uri, oidc_discovery_endpoint, require_email, user_info_map_from_id_token_payload_user_id, user_info_map_from_id_token_payload_email, user_info_map_from_id_token_payload_email_verified, user_info_map_from_user_info_endpoint_user_id, user_info_map_from_user_info_endpoint_email, user_info_map_from_user_info_endpoint_email_verified FROM " + + getConfig(start).getTenantThirdPartyProvidersTable() + ";"; + + execute(start, QUERY, pst -> {}, result -> { + while (result.next()) { + TenantIdentifier tenantIdentifier = new TenantIdentifier(result.getString("connection_uri_domain"), result.getString("app_id"), result.getString("tenant_id")); + ThirdPartyConfig.ProviderClient[] clients = null; + if (providerClientsMap.containsKey(tenantIdentifier) && providerClientsMap.get(tenantIdentifier).containsKey(result.getString("third_party_id"))) { + clients = providerClientsMap.get(tenantIdentifier).get(result.getString("third_party_id")).values().toArray(new ThirdPartyConfig.ProviderClient[0]); + } + ThirdPartyConfig.Provider provider = ThirdPartyProviderSQLHelper.TenantThirdPartyProviderRowMapper.getInstance(clients).mapOrThrow(result); + + if (!providerMap.containsKey(tenantIdentifier)) { + providerMap.put(tenantIdentifier, new HashMap<>()); + } + providerMap.get(tenantIdentifier).put(provider.thirdPartyId, provider); + } + return null; + }); + return providerMap; + } + + public static void create(Start start, Connection sqlCon, TenantConfig tenantConfig, ThirdPartyConfig.Provider provider) + throws SQLException, StorageQueryException { + String QUERY = "INSERT INTO " + getConfig(start).getTenantThirdPartyProvidersTable() + + "(connection_uri_domain, app_id, tenant_id, third_party_id, name, authorization_endpoint, authorization_endpoint_query_params, token_endpoint, token_endpoint_body_params, user_info_endpoint, user_info_endpoint_query_params, user_info_endpoint_headers, jwks_uri, oidc_discovery_endpoint, require_email, user_info_map_from_id_token_payload_user_id, user_info_map_from_id_token_payload_email, user_info_map_from_id_token_payload_email_verified, user_info_map_from_user_info_endpoint_user_id, user_info_map_from_user_info_endpoint_email, user_info_map_from_user_info_endpoint_email_verified)" + " VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + + update(sqlCon, QUERY, pst -> { + pst.setString(1, tenantConfig.tenantIdentifier.getConnectionUriDomain()); + pst.setString(2, tenantConfig.tenantIdentifier.getAppId()); + pst.setString(3, tenantConfig.tenantIdentifier.getTenantId()); + pst.setString(4, provider.thirdPartyId); + pst.setString(5, provider.name); + pst.setString(6, provider.authorizationEndpoint); + pst.setString(7, JsonUtils.jsonObjectToString(provider.authorizationEndpointQueryParams)); + pst.setString(8, provider.tokenEndpoint); + pst.setString(9, JsonUtils.jsonObjectToString(provider.tokenEndpointBodyParams)); + pst.setString(10, provider.userInfoEndpoint); + pst.setString(11, JsonUtils.jsonObjectToString(provider.userInfoEndpointQueryParams)); + pst.setString(12, JsonUtils.jsonObjectToString(provider.userInfoEndpointHeaders)); + pst.setString(13, provider.jwksURI); + pst.setString(14, provider.oidcDiscoveryEndpoint); + if (provider.requireEmail == null) { + pst.setNull(15, Types.BOOLEAN); + } else { + pst.setBoolean(15, provider.requireEmail.booleanValue()); + } + pst.setString(16, provider.userInfoMap.fromIdTokenPayload.userId); + pst.setString(17, provider.userInfoMap.fromIdTokenPayload.email); + pst.setString(18, provider.userInfoMap.fromIdTokenPayload.emailVerified); + pst.setString(19, provider.userInfoMap.fromUserInfoAPI.userId); + pst.setString(20, provider.userInfoMap.fromUserInfoAPI.email); + pst.setString(21, provider.userInfoMap.fromUserInfoAPI.emailVerified); + }); + } +} diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/utils/JsonUtils.java b/src/main/java/io/supertokens/storage/postgresql/queries/utils/JsonUtils.java new file mode 100644 index 00000000..04908932 --- /dev/null +++ b/src/main/java/io/supertokens/storage/postgresql/queries/utils/JsonUtils.java @@ -0,0 +1,37 @@ +/* + * 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.storage.postgresql.queries.utils; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +public class JsonUtils { + public static String jsonObjectToString(JsonObject obj) { + if (obj == null) { + return null; + } + return obj.toString(); + } + + public static JsonObject stringToJsonObject(String json) { + if (json == null) { + return null; + } + JsonParser jp = new JsonParser(); + return jp.parse(json).getAsJsonObject(); + } +} diff --git a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java index 14ddbce0..b3a81be1 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java @@ -25,6 +25,7 @@ import io.supertokens.pluginInterface.Storage; import io.supertokens.pluginInterface.exceptions.DbInitException; import io.supertokens.pluginInterface.exceptions.InvalidConfigException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.*; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.storage.postgresql.Start; @@ -33,8 +34,14 @@ import io.supertokens.storageLayer.StorageLayer; import org.junit.*; import org.junit.rules.TestRule; +import org.postgresql.util.PSQLException; import java.io.IOException; +import java.sql.SQLTransientConnectionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import static org.junit.Assert.*; From 4525decdf12eb07c988cd99fe7a46c2fd48868ad Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Tue, 14 Mar 2023 13:59:51 +0530 Subject: [PATCH 040/106] fix: Multitenant emailpassword recipe changes (#60) * fix: emailpassword schema * fix: ep, ev and pless schema * fix: prepare for ep review * fix: app_id_to_user_id table * fix: ep recipe impl * fix: removed todo * fix: updated as per plugin interface * fix: fixed index * fix: pr comments * fix: removed backward compatibility --- .../supertokens/storage/postgresql/Start.java | 92 ++---- .../postgresql/config/PostgreSQLConfig.java | 6 +- .../queries/EmailPasswordQueries.java | 296 +++++++++++++----- .../postgresql/queries/GeneralQueries.java | 59 ++-- .../queries/UserIdMappingQueries.java | 108 ++++--- 5 files changed, 346 insertions(+), 215 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 5ef12eb2..4ba536f8 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -723,41 +723,32 @@ public void modifyConfigToAddANewUserPoolForTesting(JsonObject config, int poolN @Override public void signUp(TenantIdentifier tenantIdentifier, UserInfo userInfo) throws StorageQueryException, DuplicateUserIdException, DuplicateEmailException { - // TODO.. try { - EmailPasswordQueries.signUp(this, userInfo.id, userInfo.email, userInfo.passwordHash, userInfo.timeJoined); + EmailPasswordQueries.signUp(this, tenantIdentifier, userInfo.id, userInfo.email, userInfo.passwordHash, userInfo.timeJoined); } catch (StorageTransactionLogicException eTemp) { Exception e = eTemp.actualException; if (e instanceof PSQLException) { PostgreSQLConfig config = Config.getConfig(this); ServerErrorMessage serverMessage = ((PSQLException) e).getServerErrorMessage(); - if (isUniqueConstraintError(serverMessage, config.getEmailPasswordUsersTable(), "email")) { + if (isUniqueConstraintError(serverMessage, config.getEmailPasswordUserToTenantTable(), "email")) { throw new DuplicateEmailException(); } else if (isPrimaryKeyError(serverMessage, config.getEmailPasswordUsersTable()) - || isPrimaryKeyError(serverMessage, config.getUsersTable())) { + || isPrimaryKeyError(serverMessage, config.getUsersTable()) + || isPrimaryKeyError(serverMessage, config.getEmailPasswordUserToTenantTable()) + || isPrimaryKeyError(serverMessage, config.getAppIdToUserIdTable())) { throw new DuplicateUserIdException(); } } - // We keep the old exception detection logic to ensure backwards compatibility. - // We could get here if the new logic hits a false negative, - // e.g., in case someone renamed constraints/tables - if (e.getMessage().contains("ERROR: duplicate key") && e.getMessage().contains("Key (email)")) { - throw new DuplicateEmailException(); - } else if (e.getMessage().contains("ERROR: duplicate key") && e.getMessage().contains("Key (user_id)")) { - throw new DuplicateUserIdException(); - } - throw new StorageQueryException(e); } } @Override public void deleteEmailPasswordUser(AppIdentifier appIdentifier, String userId) throws StorageQueryException { - // TODO.. try { - EmailPasswordQueries.deleteUser(this, userId); + EmailPasswordQueries.deleteUser(this, appIdentifier, userId); } catch (StorageTransactionLogicException e) { throw new StorageQueryException(e.actualException); } @@ -765,9 +756,8 @@ public void deleteEmailPasswordUser(AppIdentifier appIdentifier, String userId) @Override public UserInfo getUserInfoUsingId(AppIdentifier appIdentifier, String id) throws StorageQueryException { - // TODO.. try { - return EmailPasswordQueries.getUserInfoUsingId(this, id); + return EmailPasswordQueries.getUserInfoUsingId(this, appIdentifier, id); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -776,9 +766,8 @@ public UserInfo getUserInfoUsingId(AppIdentifier appIdentifier, String id) throw @Override public UserInfo getUserInfoUsingEmail(TenantIdentifier tenantIdentifier, String email) throws StorageQueryException { - // TODO.. try { - return EmailPasswordQueries.getUserInfoUsingEmail(this, email); + return EmailPasswordQueries.getUserInfoUsingEmail(this, tenantIdentifier, email); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -787,9 +776,8 @@ public UserInfo getUserInfoUsingEmail(TenantIdentifier tenantIdentifier, String @Override public void addPasswordResetToken(AppIdentifier appIdentifier, PasswordResetTokenInfo passwordResetTokenInfo) throws StorageQueryException, UnknownUserIdException, DuplicatePasswordResetTokenException { - // TODO.. try { - EmailPasswordQueries.addPasswordResetToken(this, passwordResetTokenInfo.userId, + EmailPasswordQueries.addPasswordResetToken(this, appIdentifier, passwordResetTokenInfo.userId, passwordResetTokenInfo.token, passwordResetTokenInfo.tokenExpiry); } catch (SQLException e) { if (e instanceof PSQLException) { @@ -803,14 +791,6 @@ public void addPasswordResetToken(AppIdentifier appIdentifier, PasswordResetToke } } - // We keep the old exception detection logic to ensure backwards compatibility. - // We could get here if the new logic hits a false negative, - // e.g., in case someone renamed constraints/tables - if (e.getMessage().contains("ERROR: duplicate key") && e.getMessage().contains("Key (user_id, token)")) { - throw new DuplicatePasswordResetTokenException(); - } else if (e.getMessage().contains("foreign key") && e.getMessage().contains("user_id")) { - throw new UnknownUserIdException(); - } throw new StorageQueryException(e); } } @@ -818,9 +798,8 @@ public void addPasswordResetToken(AppIdentifier appIdentifier, PasswordResetToke @Override public PasswordResetTokenInfo getPasswordResetTokenInfo(AppIdentifier appIdentifier, String token) throws StorageQueryException { - // TODO.. try { - return EmailPasswordQueries.getPasswordResetTokenInfo(this, token); + return EmailPasswordQueries.getPasswordResetTokenInfo(this, appIdentifier, token); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -829,9 +808,8 @@ public PasswordResetTokenInfo getPasswordResetTokenInfo(AppIdentifier appIdentif @Override public PasswordResetTokenInfo[] getAllPasswordResetTokenInfoForUser(AppIdentifier appIdentifier, String userId) throws StorageQueryException { - // TODO.. try { - return EmailPasswordQueries.getAllPasswordResetTokenInfoForUser(this, userId); + return EmailPasswordQueries.getAllPasswordResetTokenInfoForUser(this, appIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -842,10 +820,9 @@ public PasswordResetTokenInfo[] getAllPasswordResetTokenInfoForUser_Transaction( TransactionConnection con, String userId) throws StorageQueryException { - // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { - return EmailPasswordQueries.getAllPasswordResetTokenInfoForUser_Transaction(this, sqlCon, userId); + return EmailPasswordQueries.getAllPasswordResetTokenInfoForUser_Transaction(this, sqlCon, appIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -855,10 +832,9 @@ public PasswordResetTokenInfo[] getAllPasswordResetTokenInfoForUser_Transaction( public void deleteAllPasswordResetTokensForUser_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId) throws StorageQueryException { - // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { - EmailPasswordQueries.deleteAllPasswordResetTokensForUser_Transaction(this, sqlCon, userId); + EmailPasswordQueries.deleteAllPasswordResetTokensForUser_Transaction(this, sqlCon, appIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -868,10 +844,9 @@ public void deleteAllPasswordResetTokensForUser_Transaction(AppIdentifier appIde public void updateUsersPassword_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId, String newPassword) throws StorageQueryException { - // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { - EmailPasswordQueries.updateUsersPassword_Transaction(this, sqlCon, userId, newPassword); + EmailPasswordQueries.updateUsersPassword_Transaction(this, sqlCon, appIdentifier, userId, newPassword); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -881,22 +856,15 @@ public void updateUsersPassword_Transaction(AppIdentifier appIdentifier, Transac public void updateUsersEmail_Transaction(AppIdentifier appIdentifier, TransactionConnection conn, String userId, String email) throws StorageQueryException, DuplicateEmailException { - // TODO... Connection sqlCon = (Connection) conn.getConnection(); try { - EmailPasswordQueries.updateUsersEmail_Transaction(this, sqlCon, userId, email); + EmailPasswordQueries.updateUsersEmail_Transaction(this, sqlCon, appIdentifier, userId, email); } catch (SQLException e) { if (e instanceof PSQLException && isUniqueConstraintError(((PSQLException) e).getServerErrorMessage(), - Config.getConfig(this).getEmailPasswordUsersTable(), "email")) { + Config.getConfig(this).getEmailPasswordUserToTenantTable(), "email")) { throw new DuplicateEmailException(); } - // We keep the old exception detection logic to ensure backwards compatibility. - // We could get here if the new logic hits a false negative, - // e.g., in case someone renamed constraints/tables - if (e.getMessage().contains("ERROR: duplicate key") && e.getMessage().contains("Key (email)")) { - throw new DuplicateEmailException(); - } throw new StorageQueryException(e); } } @@ -905,10 +873,9 @@ public void updateUsersEmail_Transaction(AppIdentifier appIdentifier, Transactio public UserInfo getUserInfoUsingId_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId) throws StorageQueryException { - // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { - return EmailPasswordQueries.getUserInfoUsingId_Transaction(this, sqlCon, userId); + return EmailPasswordQueries.getUserInfoUsingId_Transaction(this, sqlCon, appIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1199,9 +1166,8 @@ public AuthRecipeUserInfo[] getUsers(TenantIdentifier tenantIdentifier, @NotNull @Nullable RECIPE_ID[] includeRecipeIds, @Nullable String userId, @Nullable Long timeJoined) throws StorageQueryException { - // TODO.. try { - return GeneralQueries.getUsers(this, limit, timeJoinedOrder, includeRecipeIds, userId, timeJoined); + return GeneralQueries.getUsers(this, tenantIdentifier, limit, timeJoinedOrder, includeRecipeIds, userId, timeJoined); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1943,7 +1909,7 @@ public void createUserIdMapping(AppIdentifier appIdentifier, String superTokensU throws StorageQueryException, UnknownSuperTokensUserIdException, UserIdMappingAlreadyExistsException { // TODO.. try { - UserIdMappingQueries.createUserIdMapping(this, superTokensUserId, externalUserId, externalUserIdInfo); + UserIdMappingQueries.createUserIdMapping(this, appIdentifier, superTokensUserId, externalUserId, externalUserIdInfo); } catch (SQLException e) { if (e instanceof PSQLException) { PostgreSQLConfig config = Config.getConfig(this); @@ -1977,10 +1943,10 @@ public boolean deleteUserIdMapping(AppIdentifier appIdentifier, String userId, b // TODO.. try { if (isSuperTokensUserId) { - return UserIdMappingQueries.deleteUserIdMappingWithSuperTokensUserId(this, userId); + return UserIdMappingQueries.deleteUserIdMappingWithSuperTokensUserId(this, appIdentifier, userId); } - return UserIdMappingQueries.deleteUserIdMappingWithExternalUserId(this, userId); + return UserIdMappingQueries.deleteUserIdMappingWithExternalUserId(this, appIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1992,10 +1958,10 @@ public UserIdMapping getUserIdMapping(AppIdentifier appIdentifier, String userId // TODO.. try { if (isSuperTokensUserId) { - return UserIdMappingQueries.getuseraIdMappingWithSuperTokensUserId(this, userId); + return UserIdMappingQueries.getuseraIdMappingWithSuperTokensUserId(this, appIdentifier, userId); } - return UserIdMappingQueries.getUserIdMappingWithExternalUserId(this, userId); + return UserIdMappingQueries.getUserIdMappingWithExternalUserId(this, appIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -2006,7 +1972,7 @@ public UserIdMapping[] getUserIdMapping(AppIdentifier appIdentifier, String user throws StorageQueryException { // TODO.. try { - return UserIdMappingQueries.getUserIdMappingWithEitherSuperTokensUserIdOrExternalUserId(this, userId); + return UserIdMappingQueries.getUserIdMappingWithEitherSuperTokensUserIdOrExternalUserId(this, appIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -2020,12 +1986,12 @@ public boolean updateOrDeleteExternalUserIdInfo(AppIdentifier appIdentifier, Str // TODO.. try { if (isSuperTokensUserId) { - return UserIdMappingQueries.updateOrDeleteExternalUserIdInfoWithSuperTokensUserId(this, userId, - externalUserIdInfo); + return UserIdMappingQueries.updateOrDeleteExternalUserIdInfoWithSuperTokensUserId(this, + appIdentifier, userId, externalUserIdInfo); } - return UserIdMappingQueries.updateOrDeleteExternalUserIdInfoWithExternalUserId(this, userId, - externalUserIdInfo); + return UserIdMappingQueries.updateOrDeleteExternalUserIdInfoWithExternalUserId(this, + appIdentifier, userId, externalUserIdInfo); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -2037,7 +2003,7 @@ public HashMap getUserIdMappingForSuperTokensIds(AppIdentifier a throws StorageQueryException { // TODO.. try { - return UserIdMappingQueries.getUserIdMappingWithUserIds(this, userIds); + return UserIdMappingQueries.getUserIdMappingWithUserIds(this, appIdentifier, userIds); } catch (SQLException e) { throw new StorageQueryException(e); } diff --git a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java index 4c99642c..df7cd0a0 100644 --- a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java +++ b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java @@ -265,6 +265,11 @@ public String getSessionInfoTable() { return addSchemaAndPrefixToTableName(tableName); } + public String getEmailPasswordUserToTenantTable() { + String tableName = "emailpassword_user_to_tenant"; + return addSchemaAndPrefixToTableName(tableName); + } + public String getEmailPasswordUsersTable() { String tableName = "emailpassword_users"; if (postgresql_emailpassword_users_table_name != null) { @@ -308,7 +313,6 @@ public String getThirdPartyUsersTable() { public String getPasswordlessUsersTable() { String tableName = "passwordless_users"; return addSchemaAndPrefixToTableName(tableName); - } public String getPasswordlessDevicesTable() { diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java index 0f1479f4..c6b6e10c 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java @@ -21,6 +21,8 @@ import io.supertokens.pluginInterface.emailpassword.UserInfo; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.config.Config; import io.supertokens.storage.postgresql.utils.Utils; @@ -39,18 +41,41 @@ import static java.lang.System.currentTimeMillis; public class EmailPasswordQueries { - static String getQueryToCreateUsersTable(Start start) { String schema = Config.getConfig(start).getTableSchema(); String emailPasswordUsersTable = Config.getConfig(start).getEmailPasswordUsersTable(); // @formatter:off return "CREATE TABLE IF NOT EXISTS " + emailPasswordUsersTable + " (" + + "app_id VARCHAR(64) DEFAULT 'public'," + "user_id CHAR(36) NOT NULL," - + "email VARCHAR(256) NOT NULL CONSTRAINT " + - Utils.getConstraintName(schema, emailPasswordUsersTable, "email", "key") + " UNIQUE," + + "email VARCHAR(256) NOT NULL," + "password_hash VARCHAR(256) NOT NULL," + "time_joined BIGINT NOT NULL," - + "CONSTRAINT " + Utils.getConstraintName(schema, emailPasswordUsersTable, null, "pkey") + - " PRIMARY KEY (user_id));"; + + "CONSTRAINT " + Utils.getConstraintName(schema, emailPasswordUsersTable, "app_id", "fkey") + + " FOREIGN KEY(app_id, user_id)" + + " REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable() + " (app_id, user_id) ON DELETE CASCADE," + + "CONSTRAINT " + Utils.getConstraintName(schema, emailPasswordUsersTable, null, "pkey") + + " PRIMARY KEY (app_id, user_id)" + + ");"; + // @formatter:on + } + + static String getQueryToCreateEmailPasswordUserToTenantTable(Start start) { + String schema = Config.getConfig(start).getTableSchema(); + String emailPasswordUserToTenantTable = Config.getConfig(start).getEmailPasswordUserToTenantTable(); + // @formatter:off + return "CREATE TABLE IF NOT EXISTS " + emailPasswordUserToTenantTable + " (" + + "app_id VARCHAR(64) DEFAULT 'public'," + + "tenant_id VARCHAR(64) DEFAULT 'public'," + + "user_id CHAR(36) NOT NULL," + + "email VARCHAR(256) NOT NULL," + + "CONSTRAINT " + Utils.getConstraintName(schema, emailPasswordUserToTenantTable, "email", "key") + + " UNIQUE (app_id, tenant_id, email)," + + "CONSTRAINT " + Utils.getConstraintName(schema, emailPasswordUserToTenantTable, null, "pkey") + + " PRIMARY KEY (app_id, tenant_id, user_id)," + + "CONSTRAINT " + Utils.getConstraintName(schema, emailPasswordUserToTenantTable, "user_id", "fkey") + + " FOREIGN KEY (app_id, tenant_id, user_id)" + + " REFERENCES " + Config.getConfig(start).getUsersTable() + "(app_id, tenant_id, user_id) ON DELETE CASCADE" + + ");"; // @formatter:on } @@ -59,16 +84,18 @@ static String getQueryToCreatePasswordResetTokensTable(Start start) { String passwordResetTokensTable = Config.getConfig(start).getPasswordResetTokensTable(); // @formatter:off return "CREATE TABLE IF NOT EXISTS " + passwordResetTokensTable + " (" + + "app_id VARCHAR(64) DEFAULT 'public'," + "user_id CHAR(36) NOT NULL," - + "token VARCHAR(128) NOT NULL CONSTRAINT " + - Utils.getConstraintName(schema, passwordResetTokensTable, "token", "key") + " UNIQUE," + + "token VARCHAR(128) NOT NULL" + + " CONSTRAINT " + Utils.getConstraintName(schema, passwordResetTokensTable, "token", "key") + " UNIQUE," + "token_expiry BIGINT NOT NULL," - + "CONSTRAINT " + Utils.getConstraintName(schema, passwordResetTokensTable, null, "pkey") + - " PRIMARY KEY (user_id, token)," - + ("CONSTRAINT " + Utils.getConstraintName(schema, passwordResetTokensTable, "user_id", "fkey") + - " FOREIGN KEY (user_id)" - + " REFERENCES " + Config.getConfig(start).getEmailPasswordUsersTable() + "(user_id)" - + " ON DELETE CASCADE ON UPDATE CASCADE);"); + + "CONSTRAINT " + Utils.getConstraintName(schema, passwordResetTokensTable, null, "pkey") + + " PRIMARY KEY (app_id, user_id, token)," + + "CONSTRAINT " + Utils.getConstraintName(schema, passwordResetTokensTable, "user_id", "fkey") + + " FOREIGN KEY (app_id, user_id)" + + " REFERENCES " + Config.getConfig(start).getEmailPasswordUsersTable() + "(app_id, user_id)" + + " ON DELETE CASCADE ON UPDATE CASCADE" + + ");"; // @formatter:on } @@ -83,40 +110,63 @@ public static void deleteExpiredPasswordResetTokens(Start start) throws SQLExcep update(start, QUERY, pst -> pst.setLong(1, currentTimeMillis())); } - public static void updateUsersPassword_Transaction(Start start, Connection con, String userId, String newPassword) + public static void updateUsersPassword_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String userId, String newPassword) throws SQLException, StorageQueryException { String QUERY = "UPDATE " + getConfig(start).getEmailPasswordUsersTable() - + " SET password_hash = ? WHERE user_id = ?"; + + " SET password_hash = ? WHERE app_id = ? AND user_id = ?"; update(con, QUERY, pst -> { pst.setString(1, newPassword); - pst.setString(2, userId); + pst.setString(2, appIdentifier.getAppId()); + pst.setString(3, userId); }); } - public static void updateUsersEmail_Transaction(Start start, Connection con, String userId, String newEmail) + public static void updateUsersEmail_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String userId, String newEmail) throws SQLException, StorageQueryException { - String QUERY = "UPDATE " + getConfig(start).getEmailPasswordUsersTable() + " SET email = ? WHERE user_id = ?"; - - update(con, QUERY, pst -> { - pst.setString(1, newEmail); - pst.setString(2, userId); - }); + { + String QUERY = "UPDATE " + getConfig(start).getEmailPasswordUsersTable() + + " SET email = ? WHERE app_id = ? AND user_id = ?"; + + update(con, QUERY, pst -> { + pst.setString(1, newEmail); + pst.setString(2, appIdentifier.getAppId()); + pst.setString(3, userId); + }); + } + { + String QUERY = "UPDATE " + getConfig(start).getEmailPasswordUserToTenantTable() + + " SET email = ? WHERE app_id = ? AND user_id = ?"; + + update(con, QUERY, pst -> { + pst.setString(1, newEmail); + pst.setString(2, appIdentifier.getAppId()); + pst.setString(3, userId); + }); + } } - public static void deleteAllPasswordResetTokensForUser_Transaction(Start start, Connection con, String userId) + public static void deleteAllPasswordResetTokensForUser_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { - String QUERY = "DELETE FROM " + getConfig(start).getPasswordResetTokensTable() + " WHERE user_id = ?"; - update(con, QUERY, pst -> pst.setString(1, userId)); + String QUERY = "DELETE FROM " + getConfig(start).getPasswordResetTokensTable() + " WHERE app_id = ? AND user_id = ?"; + + update(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); } - public static PasswordResetTokenInfo[] getAllPasswordResetTokenInfoForUser(Start start, String userId) + public static PasswordResetTokenInfo[] getAllPasswordResetTokenInfoForUser(Start start, AppIdentifier appIdentifier, + String userId) throws StorageQueryException, SQLException { String QUERY = "SELECT user_id, token, token_expiry FROM " + getConfig(start).getPasswordResetTokensTable() - + " WHERE user_id = ?"; + + " WHERE app_id = ? AND user_id = ?"; - return execute(start, QUERY, pst -> pst.setString(1, userId), result -> { + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }, result -> { List temp = new ArrayList<>(); while (result.next()) { temp.add(PasswordResetRowMapper.getInstance().mapOrThrow(result)); @@ -130,13 +180,17 @@ public static PasswordResetTokenInfo[] getAllPasswordResetTokenInfoForUser(Start } public static PasswordResetTokenInfo[] getAllPasswordResetTokenInfoForUser_Transaction(Start start, Connection con, + AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { String QUERY = "SELECT user_id, token, token_expiry FROM " + getConfig(start).getPasswordResetTokensTable() - + " WHERE user_id = ? FOR UPDATE"; + + " WHERE app_id = ? AND user_id = ? FOR UPDATE"; - return execute(con, QUERY, pst -> pst.setString(1, userId), result -> { + return execute(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }, result -> { List temp = new ArrayList<>(); while (result.next()) { temp.add(PasswordResetRowMapper.getInstance().mapOrThrow(result)); @@ -149,11 +203,16 @@ public static PasswordResetTokenInfo[] getAllPasswordResetTokenInfoForUser_Trans }); } - public static UserInfo getUserInfoUsingId_Transaction(Start start, Connection con, String id) + public static UserInfo getUserInfoUsingId_Transaction(Start start, Connection con, AppIdentifier appIdentifier, + String id) throws SQLException, StorageQueryException { String QUERY = "SELECT user_id, email, password_hash, time_joined FROM " - + getConfig(start).getEmailPasswordUsersTable() + " WHERE user_id = ? FOR UPDATE"; - return execute(con, QUERY, pst -> pst.setString(1, id), result -> { + + getConfig(start).getEmailPasswordUsersTable() + + " WHERE app_id = ? AND user_id = ? FOR UPDATE"; + return execute(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, id); + }, result -> { if (result.next()) { return UserInfoRowMapper.getInstance().mapOrThrow(result); } @@ -161,11 +220,14 @@ public static UserInfo getUserInfoUsingId_Transaction(Start start, Connection co }); } - public static PasswordResetTokenInfo getPasswordResetTokenInfo(Start start, String token) + public static PasswordResetTokenInfo getPasswordResetTokenInfo(Start start, AppIdentifier appIdentifier, String token) throws SQLException, StorageQueryException { String QUERY = "SELECT user_id, token, token_expiry FROM " + getConfig(start).getPasswordResetTokensTable() - + " WHERE token = ?"; - return execute(start, QUERY, pst -> pst.setString(1, token), result -> { + + " WHERE app_id = ? AND token = ?"; + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, token); + }, result -> { if (result.next()) { return PasswordResetRowMapper.getInstance().mapOrThrow(result); } @@ -173,42 +235,67 @@ public static PasswordResetTokenInfo getPasswordResetTokenInfo(Start start, Stri }); } - public static void addPasswordResetToken(Start start, String userId, String tokenHash, long expiry) + public static void addPasswordResetToken(Start start, AppIdentifier appIdentifier, String userId, String tokenHash, long expiry) throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + getConfig(start).getPasswordResetTokensTable() - + "(user_id, token, token_expiry)" + " VALUES(?, ?, ?)"; + + "(app_id, user_id, token, token_expiry)" + " VALUES(?, ?, ?, ?)"; update(start, QUERY, pst -> { - pst.setString(1, userId); - pst.setString(2, tokenHash); - pst.setLong(3, expiry); + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + pst.setString(3, tokenHash); + pst.setLong(4, expiry); }); } - public static void signUp(Start start, String userId, String email, String passwordHash, long timeJoined) + public static void signUp(Start start, TenantIdentifier tenantIdentifier, String userId, String email, String passwordHash, long timeJoined) throws StorageQueryException, StorageTransactionLogicException { start.startTransaction(con -> { Connection sqlCon = (Connection) con.getConnection(); try { - { + { // app_id_to_user_id + String QUERY = "INSERT INTO " + getConfig(start).getAppIdToUserIdTable() + + "(app_id, user_id)" + " VALUES(?, ?)"; + update(sqlCon, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, userId); + }); + } + + { // all_auth_recipe_users String QUERY = "INSERT INTO " + getConfig(start).getUsersTable() - + "(user_id, recipe_id, time_joined)" + " VALUES(?, ?, ?)"; + + "(app_id, tenant_id, user_id, recipe_id, time_joined)" + " VALUES(?, ?, ?, ?, ?)"; update(sqlCon, QUERY, pst -> { - pst.setString(1, userId); - pst.setString(2, EMAIL_PASSWORD.toString()); - pst.setLong(3, timeJoined); + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, userId); + pst.setString(4, EMAIL_PASSWORD.toString()); + pst.setLong(5, timeJoined); }); } - { + { // emailpassword_users String QUERY = "INSERT INTO " + getConfig(start).getEmailPasswordUsersTable() - + "(user_id, email, password_hash, time_joined)" + " VALUES(?, ?, ?, ?)"; + + "(app_id, user_id, email, password_hash, time_joined)" + " VALUES(?, ?, ?, ?, ?)"; update(sqlCon, QUERY, pst -> { - pst.setString(1, userId); - pst.setString(2, email); - pst.setString(3, passwordHash); - pst.setLong(4, timeJoined); + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, userId); + pst.setString(3, email); + pst.setString(4, passwordHash); + pst.setLong(5, timeJoined); + }); + } + + { // emailpassword_user_to_tenant + String QUERY = "INSERT INTO " + getConfig(start).getEmailPasswordUserToTenantTable() + + "(app_id, tenant_id, user_id, email)" + " VALUES(?, ?, ?, ?)"; + + update(sqlCon, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, userId); + pst.setString(4, email); }); } @@ -220,27 +307,22 @@ public static void signUp(Start start, String userId, String email, String passw }); } - public static void deleteUser(Start start, String userId) + public static void deleteUser(Start start, AppIdentifier appIdentifier, String userId) throws StorageQueryException, StorageTransactionLogicException { start.startTransaction(con -> { Connection sqlCon = (Connection) con.getConnection(); try { { - String QUERY = "DELETE FROM " + getConfig(start).getUsersTable() - + " WHERE user_id = ? AND recipe_id = ?"; + String QUERY = "DELETE FROM " + getConfig(start).getAppIdToUserIdTable() + + " WHERE app_id = ? AND user_id = ? AND recipe_id = ?"; update(sqlCon, QUERY, pst -> { - pst.setString(1, userId); - pst.setString(2, EMAIL_PASSWORD.toString()); + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + pst.setString(3, EMAIL_PASSWORD.toString()); }); } - { - String QUERY = "DELETE FROM " + getConfig(start).getEmailPasswordUsersTable() - + " WHERE user_id = ?"; - - update(sqlCon, QUERY, pst -> pst.setString(1, userId)); - } sqlCon.commit(); } catch (SQLException throwables) { throw new StorageTransactionLogicException(throwables); @@ -249,22 +331,32 @@ public static void deleteUser(Start start, String userId) }); } - public static UserInfo getUserInfoUsingId(Start start, String id) throws SQLException, StorageQueryException { - List input = new ArrayList<>(); - input.add(id); - List result = getUsersInfoUsingIdList(start, input); - if (result.size() == 1) { - return result.get(0); - } - return null; + public static UserInfo getUserInfoUsingId(Start start, AppIdentifier appIdentifier, String id) throws SQLException, StorageQueryException { + String QUERY = "SELECT user_id, email, password_hash, time_joined FROM " + + getConfig(start).getEmailPasswordUsersTable() + " WHERE app_id = ? AND user_id = ?"; + + return execute(start, QUERY.toString(), pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, id); + }, result -> { + if (result.next()) { + return UserInfoRowMapper.getInstance().mapOrThrow(result); + } + return null; + }); } - public static List getUsersInfoUsingIdList(Start start, List ids) + public static List getUsersInfoUsingIdList(Start start, TenantIdentifier tenantIdentifier, List ids) throws SQLException, StorageQueryException { if (ids.size() > 0) { - StringBuilder QUERY = new StringBuilder("SELECT user_id, email, password_hash, time_joined FROM " - + getConfig(start).getEmailPasswordUsersTable()); - QUERY.append(" WHERE user_id IN ("); + StringBuilder QUERY = new StringBuilder( + "SELECT ep_users.user_id as user_id, ep_users.email as email, ep_users.password_hash as password_hash, " + + "ep_users.time_joined as time_joined, ep_users_to_tenant.app_id, ep_users_to_tenant.tenant_id, ep_users_to_tenant.user_id " + + "FROM " + getConfig(start).getEmailPasswordUsersTable() + " AS ep_users " + + "JOIN " + getConfig(start).getEmailPasswordUserToTenantTable() + " AS ep_users_to_tenant " + + "ON ep_users.app_id = ep_users_to_tenant.app_id AND ep_users.user_id = ep_users_to_tenant.user_id" + ); + QUERY.append(" WHERE ep_users_to_tenant.app_id = ? AND ep_users_to_tenant.tenant_id = ? AND ep_users_to_tenant.user_id IN ("); for (int i = 0; i < ids.size(); i++) { QUERY.append("?"); @@ -276,9 +368,11 @@ public static List getUsersInfoUsingIdList(Start start, List i QUERY.append(")"); return execute(start, QUERY.toString(), pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); for (int i = 0; i < ids.size(); i++) { - // i+1 cause this starts with 1 and not 0 - pst.setString(i + 1, ids.get(i)); + // i+3 cause this starts with 1 and not 0, and 1 is used for app_id, 2 is used for tenant_id + pst.setString(i + 3, ids.get(i)); } }, result -> { List finalResult = new ArrayList<>(); @@ -291,15 +385,43 @@ public static List getUsersInfoUsingIdList(Start start, List i return Collections.emptyList(); } - public static UserInfo getUserInfoUsingEmail(Start start, String email) throws StorageQueryException, SQLException { - String QUERY = "SELECT user_id, email, password_hash, time_joined FROM " - + getConfig(start).getEmailPasswordUsersTable() + " WHERE email = ?"; - return execute(start, QUERY, pst -> pst.setString(1, email), result -> { - if (result.next()) { - return UserInfoRowMapper.getInstance().mapOrThrow(result); + public static UserInfo getUserInfoUsingEmail(Start start, TenantIdentifier tenantIdentifier, String email) throws StorageQueryException, SQLException { + String userId = null; + { + // check if user exists for the provided tenant + String QUERY = "SELECT user_id FROM " + + getConfig(start).getEmailPasswordUserToTenantTable() + + " WHERE app_id = ? AND tenant_id = ? AND email = ?"; + + userId = execute(start, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, email); + }, result -> { + if (result.next()) { + return result.getString("user_id"); + } + return null; + }); + + if (userId == null) { + return null; } - return null; - }); + } + { + String QUERY = "SELECT user_id, email, password_hash, time_joined FROM " + + getConfig(start).getEmailPasswordUsersTable() + " WHERE app_id = ? AND user_id = ?"; + final String userIdToQuery = userId; + return execute(start, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, userIdToQuery); + }, result -> { + if (result.next()) { + return UserInfoRowMapper.getInstance().mapOrThrow(result); + } + return null; + }); + } } private static class PasswordResetRowMapper implements RowMapper { diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java index 600e2a07..70653544 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java @@ -21,6 +21,8 @@ import io.supertokens.pluginInterface.RowMapper; import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.storage.postgresql.ConnectionPool; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.config.Config; @@ -69,17 +71,26 @@ static String getQueryToCreateUsersTable(Start start) { String usersTable = Config.getConfig(start).getUsersTable(); // @formatter:off return "CREATE TABLE IF NOT EXISTS " + usersTable + " (" + + "app_id VARCHAR(64) DEFAULT 'public'," + + "tenant_id VARCHAR(64) DEFAULT 'public'," + "user_id CHAR(36) NOT NULL," + "recipe_id VARCHAR(128) NOT NULL," + "time_joined BIGINT NOT NULL," - + "CONSTRAINT " + Utils.getConstraintName(schema, usersTable, null, "pkey") + - " PRIMARY KEY (user_id));"; + + "CONSTRAINT " + Utils.getConstraintName(schema, usersTable, null, "pkey") + + " PRIMARY KEY (app_id, tenant_id, user_id)," + + "CONSTRAINT " + Utils.getConstraintName(schema, usersTable, "tenant_id", "fkey") + + " FOREIGN KEY(app_id, tenant_id)" + + " REFERENCES " + Config.getConfig(start).getTenantsTable() + " (app_id, tenant_id) ON DELETE CASCADE," + + "CONSTRAINT " + Utils.getConstraintName(schema, usersTable, "user_id", "fkey") + + " FOREIGN KEY(app_id, user_id)" + + " REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable() + " (app_id, user_id) ON DELETE CASCADE" + + ");"; // @formatter:on } static String getQueryToCreateUserPaginationIndex(Start start) { return "CREATE INDEX all_auth_recipe_users_pagination_index ON " + Config.getConfig(start).getUsersTable() - + "(time_joined DESC, user_id " + "DESC);"; + + "(time_joined DESC, user_id DESC, tenant_id DESC, app_id DESC);"; } private static String getQueryToCreateAppsTable(Start start) { @@ -131,13 +142,11 @@ private static String getQueryToCreateAppIdToUserIdTable(Start start) { return "CREATE TABLE IF NOT EXISTS " + appToUserTable + " (" + "app_id VARCHAR(64) NOT NULL DEFAULT 'public'," + "user_id CHAR(36) NOT NULL," - + "recipe_id VARCHAR(128) NOT NULL," - + "time_joined BIGINT NOT NULL," - + "CONSTRAINT " + Utils.getConstraintName(schema, appToUserTable, null, "pkey") + - " PRIMARY KEY (app_id, user_id), " - + "CONSTRAINT " + Utils.getConstraintName(schema, appToUserTable, "app_id", "fkey") + - " FOREIGN KEY(app_id) REFERENCES " + Config.getConfig(start).getAppsTable() + - "(app_id) ON DELETE CASCADE" + + "CONSTRAINT " + Utils.getConstraintName(schema, appToUserTable, null, "pkey") + + " PRIMARY KEY (app_id, user_id), " + + "CONSTRAINT " + Utils.getConstraintName(schema, appToUserTable, "app_id", "fkey") + + " FOREIGN KEY(app_id) REFERENCES " + Config.getConfig(start).getAppsTable() + + " (app_id) ON DELETE CASCADE" + ");"; // @formatter:on } @@ -163,6 +172,11 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto update(start, getQueryToCreateKeyValueTable(start), NO_OP_SETTER); } + if (!doesTableExists(start, Config.getConfig(start).getAppIdToUserIdTable())) { + getInstance(start).addState(CREATING_NEW_TABLE, null); + update(start, getQueryToCreateAppIdToUserIdTable(start), NO_OP_SETTER); + } + if (!doesTableExists(start, Config.getConfig(start).getUsersTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); update(start, getQueryToCreateUsersTable(start), NO_OP_SETTER); @@ -171,11 +185,6 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto update(start, getQueryToCreateUserPaginationIndex(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getAppIdToUserIdTable())) { - getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, getQueryToCreateAppIdToUserIdTable(start), NO_OP_SETTER); - } - if (!doesTableExists(start, Config.getConfig(start).getAccessTokenSigningKeysTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); update(start, getQueryToCreateAccessTokenSigningKeysTable(start), NO_OP_SETTER); @@ -208,6 +217,11 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto update(start, EmailPasswordQueries.getQueryToCreateUsersTable(start), NO_OP_SETTER); } + if (!doesTableExists(start, Config.getConfig(start).getEmailPasswordUserToTenantTable())) { + getInstance(start).addState(CREATING_NEW_TABLE, null); + update(start, EmailPasswordQueries.getQueryToCreateEmailPasswordUserToTenantTable(start), NO_OP_SETTER); + } + if (!doesTableExists(start, Config.getConfig(start).getPasswordResetTokensTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); update(start, getQueryToCreatePasswordResetTokensTable(start), NO_OP_SETTER); @@ -352,6 +366,7 @@ public static void deleteAllTables(Start start) throws SQLException, StorageQuer + getConfig(start).getTenantThirdPartyProvidersTable() + "," + getConfig(start).getTenantThirdPartyProviderClientsTable() + "," + getConfig(start).getSessionInfoTable() + "," + + getConfig(start).getEmailPasswordUserToTenantTable() + "," + getConfig(start).getEmailPasswordUsersTable() + "," + getConfig(start).getPasswordResetTokensTable() + "," + getConfig(start).getEmailVerificationTokensTable() + "," @@ -455,7 +470,7 @@ public static boolean doesUserIdExist(Start start, String userId) throws SQLExce } - public static AuthRecipeUserInfo[] getUsers(Start start, @NotNull Integer limit, @NotNull String timeJoinedOrder, + public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenantIdentifier, @NotNull Integer limit, @NotNull String timeJoinedOrder, @Nullable RECIPE_ID[] includeRecipeIds, @Nullable String userId, @Nullable Long timeJoined) throws SQLException, StorageQueryException { @@ -538,8 +553,8 @@ public static AuthRecipeUserInfo[] getUsers(Start start, @NotNull Integer limit, // we give the userId[] for each recipe to fetch all those user's details for (RECIPE_ID recipeId : recipeIdToUserIdListMap.keySet()) { - List users = getUserInfoForRecipeIdFromUserIds(start, recipeId, - recipeIdToUserIdListMap.get(recipeId)); + List users = getUserInfoForRecipeIdFromUserIds(start, + tenantIdentifier, recipeId, recipeIdToUserIdListMap.get(recipeId)); // we fill in all the slots in finalResult based on their position in // usersFromQuery @@ -557,15 +572,15 @@ public static AuthRecipeUserInfo[] getUsers(Start start, @NotNull Integer limit, return finalResult; } - private static List getUserInfoForRecipeIdFromUserIds(Start start, RECIPE_ID recipeId, + private static List getUserInfoForRecipeIdFromUserIds(Start start, TenantIdentifier tenantIdentifier, RECIPE_ID recipeId, List userIds) throws StorageQueryException, SQLException { if (recipeId == RECIPE_ID.EMAIL_PASSWORD) { - return EmailPasswordQueries.getUsersInfoUsingIdList(start, userIds); + return EmailPasswordQueries.getUsersInfoUsingIdList(start, tenantIdentifier, userIds); } else if (recipeId == RECIPE_ID.THIRD_PARTY) { - return ThirdPartyQueries.getUsersInfoUsingIdList(start, userIds); + return ThirdPartyQueries.getUsersInfoUsingIdList(start, userIds); // TODO pass tenantIdentifier } else if (recipeId == RECIPE_ID.PASSWORDLESS) { - return PasswordlessQueries.getUsersByIdList(start, userIds); + return PasswordlessQueries.getUsersByIdList(start, userIds); // TODO pass tenantIdentifier } else { throw new IllegalArgumentException("No implementation of get users for recipe: " + recipeId.toString()); } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/UserIdMappingQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/UserIdMappingQueries.java index 26d031db..dd577cf2 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/UserIdMappingQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/UserIdMappingQueries.java @@ -18,6 +18,7 @@ import io.supertokens.pluginInterface.RowMapper; import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.useridmapping.UserIdMapping; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.config.Config; @@ -39,37 +40,44 @@ public static String getQueryToCreateUserIdMappingTable(Start start) { String userIdMappingTable = Config.getConfig(start).getUserIdMappingTable(); // @formatter:off return "CREATE TABLE IF NOT EXISTS " + userIdMappingTable + " (" - + "supertokens_user_id CHAR(36) NOT NULL " - + "CONSTRAINT " + Utils.getConstraintName(schema, userIdMappingTable, "supertokens_user_id", "key") + " UNIQUE," - + "external_user_id VARCHAR(128) NOT NULL" - + " CONSTRAINT " + Utils.getConstraintName(schema, userIdMappingTable, "external_user_id", "key") + " UNIQUE," + + "app_id VARCHAR(64) DEFAULT 'public'," + + "supertokens_user_id CHAR(36) NOT NULL," + + "external_user_id VARCHAR(128) NOT NULL," + + "CONSTRAINT " + Utils.getConstraintName(schema, userIdMappingTable, "supertokens_user_id", "key") + + " UNIQUE (app_id, supertokens_user_id)," + + "CONSTRAINT " + Utils.getConstraintName(schema, userIdMappingTable, "external_user_id", "key") + + " UNIQUE (app_id, external_user_id)," + "external_user_id_info TEXT," - + " CONSTRAINT " + Utils.getConstraintName(schema, userIdMappingTable, null, "pkey") + - " PRIMARY KEY(supertokens_user_id, external_user_id)," - + ("CONSTRAINT " + Utils.getConstraintName(schema, userIdMappingTable, "supertokens_user_id", "fkey") + - " FOREIGN KEY (supertokens_user_id)" - + " REFERENCES " + Config.getConfig(start).getUsersTable() + "(user_id)" - + " ON DELETE CASCADE);"); + + "CONSTRAINT " + Utils.getConstraintName(schema, userIdMappingTable, null, "pkey") + + " PRIMARY KEY(app_id, supertokens_user_id, external_user_id)," + + "CONSTRAINT " + Utils.getConstraintName(schema, userIdMappingTable, "supertokens_user_id", "fkey") + + " FOREIGN KEY (app_id, supertokens_user_id)" + + " REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable() + "(app_id, user_id)" + " ON DELETE CASCADE" + + ");"; // @formatter:on } - public static void createUserIdMapping(Start start, String superTokensUserId, String externalUserId, - String externalUserIdInfo) throws SQLException, StorageQueryException { + public static void createUserIdMapping(Start start, AppIdentifier appIdentifier, String superTokensUserId, String externalUserId, + String externalUserIdInfo) throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + Config.getConfig(start).getUserIdMappingTable() - + " (supertokens_user_id, external_user_id, external_user_id_info)" + " VALUES(?, ?, ?)"; + + " (app_id, supertokens_user_id, external_user_id, external_user_id_info)" + " VALUES(?, ?, ?, ?)"; update(start, QUERY, pst -> { - pst.setString(1, superTokensUserId); - pst.setString(2, externalUserId); - pst.setString(3, externalUserIdInfo); + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, superTokensUserId); + pst.setString(3, externalUserId); + pst.setString(4, externalUserIdInfo); }); } - public static UserIdMapping getuseraIdMappingWithSuperTokensUserId(Start start, String userId) + public static UserIdMapping getuseraIdMappingWithSuperTokensUserId(Start start, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { String QUERY = "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable() - + " WHERE supertokens_user_id = ?"; - return execute(start, QUERY, pst -> pst.setString(1, userId), result -> { + + " WHERE app_id = ? AND supertokens_user_id = ?"; + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }, result -> { if (result.next()) { return UserIdMappingRowMapper.getInstance().mapOrThrow(result); } @@ -77,12 +85,15 @@ public static UserIdMapping getuseraIdMappingWithSuperTokensUserId(Start start, }); } - public static UserIdMapping getUserIdMappingWithExternalUserId(Start start, String userId) + public static UserIdMapping getUserIdMappingWithExternalUserId(Start start, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { String QUERY = "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable() - + " WHERE external_user_id = ?"; + + " WHERE app_id = ? AND external_user_id = ?"; - return execute(start, QUERY, pst -> pst.setString(1, userId), result -> { + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }, result -> { if (result.next()) { return UserIdMappingRowMapper.getInstance().mapOrThrow(result); } @@ -91,13 +102,14 @@ public static UserIdMapping getUserIdMappingWithExternalUserId(Start start, Stri } public static UserIdMapping[] getUserIdMappingWithEitherSuperTokensUserIdOrExternalUserId(Start start, - String userId) throws SQLException, StorageQueryException { + AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { String QUERY = "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable() - + " WHERE supertokens_user_id = ? OR external_user_id = ? "; + + " WHERE app_id = ? AND (supertokens_user_id = ? OR external_user_id = ?)"; return execute(start, QUERY, pst -> { - pst.setString(1, userId); + pst.setString(1, appIdentifier.getAppId()); pst.setString(2, userId); + pst.setString(3, userId); }, result -> { ArrayList userIdMappingArray = new ArrayList<>(); while (result.next()) { @@ -108,7 +120,8 @@ public static UserIdMapping[] getUserIdMappingWithEitherSuperTokensUserIdOrExter } - public static HashMap getUserIdMappingWithUserIds(Start start, ArrayList userIds) + public static HashMap getUserIdMappingWithUserIds(Start start, AppIdentifier appIdentifier, + ArrayList userIds) throws SQLException, StorageQueryException { if (userIds.size() == 0) { @@ -116,7 +129,7 @@ public static HashMap getUserIdMappingWithUserIds(Start start, A } StringBuilder QUERY = new StringBuilder( - "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable() + " WHERE supertokens_user_id IN ("); + "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable() + " WHERE app_id = ? AND supertokens_user_id IN ("); for (int i = 0; i < userIds.size(); i++) { QUERY.append("?"); if (i != userIds.size() - 1) { @@ -126,9 +139,10 @@ public static HashMap getUserIdMappingWithUserIds(Start start, A } QUERY.append(")"); return execute(start, QUERY.toString(), pst -> { + pst.setString(1, appIdentifier.getAppId()); for (int i = 0; i < userIds.size(); i++) { // i+1 cause this starts with 1 and not 0 - pst.setString(i + 1, userIds.get(i)); + pst.setString(i + 2, userIds.get(i)); } }, result -> { HashMap userIdMappings = new HashMap<>(); @@ -140,48 +154,58 @@ public static HashMap getUserIdMappingWithUserIds(Start start, A }); } - public static boolean deleteUserIdMappingWithSuperTokensUserId(Start start, String userId) + public static boolean deleteUserIdMappingWithSuperTokensUserId(Start start, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { String QUERY = "DELETE FROM " + Config.getConfig(start).getUserIdMappingTable() - + " WHERE supertokens_user_id = ?"; + + " WHERE app_id = ? AND supertokens_user_id = ?"; // store the number of rows updated - int rowUpdatedCount = update(start, QUERY, pst -> pst.setString(1, userId)); + int rowUpdatedCount = update(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); return rowUpdatedCount > 0; } - public static boolean deleteUserIdMappingWithExternalUserId(Start start, String userId) + public static boolean deleteUserIdMappingWithExternalUserId(Start start, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { - String QUERY = "DELETE FROM " + Config.getConfig(start).getUserIdMappingTable() + " WHERE external_user_id = ?"; + String QUERY = "DELETE FROM " + Config.getConfig(start).getUserIdMappingTable() + " WHERE app_id = ? AND external_user_id = ?"; // store the number of rows updated - int rowUpdatedCount = update(start, QUERY, pst -> pst.setString(1, userId)); + int rowUpdatedCount = update(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); return rowUpdatedCount > 0; } - public static boolean updateOrDeleteExternalUserIdInfoWithSuperTokensUserId(Start start, String userId, - @Nullable String externalUserIdInfo) throws SQLException, StorageQueryException { + public static boolean updateOrDeleteExternalUserIdInfoWithSuperTokensUserId(Start start, + AppIdentifier appIdentifier, String userId, + @Nullable String externalUserIdInfo) throws SQLException, StorageQueryException { String QUERY = "UPDATE " + Config.getConfig(start).getUserIdMappingTable() - + " SET external_user_id_info = ? WHERE supertokens_user_id = ?"; + + " SET external_user_id_info = ? WHERE app_id = ? AND supertokens_user_id = ?"; int rowUpdated = update(start, QUERY, pst -> { pst.setString(1, externalUserIdInfo); - pst.setString(2, userId); + pst.setString(2, appIdentifier.getAppId()); + pst.setString(3, userId); }); return rowUpdated > 0; } - public static boolean updateOrDeleteExternalUserIdInfoWithExternalUserId(Start start, String userId, - @Nullable String externalUserIdInfo) throws SQLException, StorageQueryException { + public static boolean updateOrDeleteExternalUserIdInfoWithExternalUserId(Start start, AppIdentifier appIdentifier, + String userId, + @Nullable String externalUserIdInfo) throws SQLException, StorageQueryException { String QUERY = "UPDATE " + Config.getConfig(start).getUserIdMappingTable() - + " SET external_user_id_info = ? WHERE external_user_id = ?"; + + " SET external_user_id_info = ? WHERE app_id = ? AND external_user_id = ?"; int rowUpdated = update(start, QUERY, pst -> { pst.setString(1, externalUserIdInfo); - pst.setString(2, userId); + pst.setString(2, appIdentifier.getAppId()); + pst.setString(3, userId); }); return rowUpdated > 0; From 639cb7c18469ed8170860c0fd203e6885c29e5af Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Fri, 17 Mar 2023 15:51:22 +0530 Subject: [PATCH 041/106] fix: minor fix (#62) --- .../storage/postgresql/queries/EmailPasswordQueries.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java index c6b6e10c..3c084e20 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java @@ -314,12 +314,11 @@ public static void deleteUser(Start start, AppIdentifier appIdentifier, String u try { { String QUERY = "DELETE FROM " + getConfig(start).getAppIdToUserIdTable() - + " WHERE app_id = ? AND user_id = ? AND recipe_id = ?"; + + " WHERE app_id = ? AND user_id = ?"; update(sqlCon, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); pst.setString(2, userId); - pst.setString(3, EMAIL_PASSWORD.toString()); }); } From baf6a863be5c42e7e3d3a05cd79d2292c0373911 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Fri, 24 Mar 2023 16:52:32 +0530 Subject: [PATCH 042/106] fix: Multitenant schema changes (#64) * fix: ev and pless impl * fix: ev fixes * fix: pless and tp changes * fix: revert delete user * fix: pless impl * fix: cleanup and fixed deleteUser * fix: simplified queries and added fkey checks in ep * fix: fkey checks for pless * fix: fkey checks for thirdparty * fix: fkey checks for emailverification * fix: fixed test * fix: updated to join query for ep * fix: updated join queries * fix: constraints * fix: test fix * fix: pr comments --- .../supertokens/storage/postgresql/Start.java | 256 ++++---- .../postgresql/config/PostgreSQLConfig.java | 10 + .../queries/EmailPasswordQueries.java | 69 +- .../queries/EmailVerificationQueries.java | 150 +++-- .../postgresql/queries/GeneralQueries.java | 21 +- .../queries/PasswordlessQueries.java | 588 +++++++++++++----- .../postgresql/queries/ThirdPartyQueries.java | 209 +++++-- .../postgresql/test/ExceptionParsingTest.java | 23 +- 8 files changed, 862 insertions(+), 464 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 4ba536f8..71abb603 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -650,7 +650,7 @@ public boolean isUserIdBeingUsedInNonAuthRecipe(AppIdentifier appIdentifier, Str return userMetadata != null; } else if (className.equals(EmailVerificationStorage.class.getName())) { try { - return EmailVerificationQueries.isUserIdBeingUsedForEmailVerification(this, userId); + return EmailVerificationQueries.isUserIdBeingUsedForEmailVerification(this, appIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -696,6 +696,8 @@ public void addInfoToNonAuthRecipesBasedOnUserId(String className, String userId } catch (DuplicateEmailVerificationTokenException e) { throw new StorageQueryException(e); + } catch (TenantOrAppNotFoundException e) { + throw new IllegalStateException(e); } } else if (className.equals(UserMetadataStorage.class.getName())) { JsonObject data = new JsonObject(); @@ -722,7 +724,8 @@ public void modifyConfigToAddANewUserPoolForTesting(JsonObject config, int poolN @Override public void signUp(TenantIdentifier tenantIdentifier, UserInfo userInfo) - throws StorageQueryException, DuplicateUserIdException, DuplicateEmailException { + throws StorageQueryException, DuplicateUserIdException, DuplicateEmailException, + TenantOrAppNotFoundException { try { EmailPasswordQueries.signUp(this, tenantIdentifier, userInfo.id, userInfo.email, userInfo.passwordHash, userInfo.timeJoined); } catch (StorageTransactionLogicException eTemp) { @@ -738,6 +741,10 @@ public void signUp(TenantIdentifier tenantIdentifier, UserInfo userInfo) || isPrimaryKeyError(serverMessage, config.getEmailPasswordUserToTenantTable()) || isPrimaryKeyError(serverMessage, config.getAppIdToUserIdTable())) { throw new DuplicateUserIdException(); + } else if (isForeignKeyConstraintError(serverMessage, config.getAppIdToUserIdTable(), "app_id")) { + throw new TenantOrAppNotFoundException(tenantIdentifier.toAppIdentifier()); + } else if (isForeignKeyConstraintError(serverMessage, config.getUsersTable(), "tenant_id")) { + throw new TenantOrAppNotFoundException(tenantIdentifier); } } @@ -895,11 +902,10 @@ public EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUser_Tran TransactionConnection con, String userId, String email) throws StorageQueryException { - // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { - return EmailVerificationQueries.getAllEmailVerificationTokenInfoForUser_Transaction(this, sqlCon, userId, - email); + return EmailVerificationQueries.getAllEmailVerificationTokenInfoForUser_Transaction(this, sqlCon, + appIdentifier, userId, email); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -909,10 +915,9 @@ public EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUser_Tran public void deleteAllEmailVerificationTokensForUser_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId, String email) throws StorageQueryException { - // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { - EmailVerificationQueries.deleteAllEmailVerificationTokensForUser_Transaction(this, sqlCon, userId, email); + EmailVerificationQueries.deleteAllEmailVerificationTokensForUser_Transaction(this, sqlCon, appIdentifier, userId, email); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -921,23 +926,26 @@ public void deleteAllEmailVerificationTokensForUser_Transaction(AppIdentifier ap @Override public void updateIsEmailVerified_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId, String email, - boolean isEmailVerified) throws StorageQueryException { - // TODO.. + boolean isEmailVerified) + throws StorageQueryException, TenantOrAppNotFoundException { Connection sqlCon = (Connection) con.getConnection(); try { - EmailVerificationQueries.updateUsersIsEmailVerified_Transaction(this, sqlCon, userId, email, + EmailVerificationQueries.updateUsersIsEmailVerified_Transaction(this, sqlCon, appIdentifier, userId, email, isEmailVerified); } catch (SQLException e) { + if (e instanceof PSQLException) { + PostgreSQLConfig config = Config.getConfig(this); + ServerErrorMessage serverMessage = ((PSQLException) e).getServerErrorMessage(); + + if (isForeignKeyConstraintError(serverMessage, config.getEmailVerificationTable(), "app_id")) { + throw new TenantOrAppNotFoundException(appIdentifier); + } + } + boolean isPSQLPrimKeyError = e instanceof PSQLException && isPrimaryKeyError( ((PSQLException) e).getServerErrorMessage(), Config.getConfig(this).getEmailVerificationTable()); - // We keep the old exception detection logic to ensure backwards compatibility. - // We could get here if the new logic hits a false negative, - // e.g., in case someone renamed constraints/tables - boolean isDuplicateKeyError = e.getMessage().contains("ERROR: duplicate key") - && e.getMessage().contains("Key (user_id, email)"); - - if (!isEmailVerified || (!isPSQLPrimKeyError && !isDuplicateKeyError)) { + if (!isEmailVerified || !isPSQLPrimKeyError) { throw new StorageQueryException(e); } // we do not throw an error since the email is already verified @@ -948,8 +956,7 @@ public void updateIsEmailVerified_Transaction(AppIdentifier appIdentifier, Trans public void deleteEmailVerificationUserInfo(AppIdentifier appIdentifier, String userId) throws StorageQueryException { try { - // TODO.. - EmailVerificationQueries.deleteUserInfo(this, userId); + EmailVerificationQueries.deleteUserInfo(this, appIdentifier, userId); } catch (StorageTransactionLogicException e) { throw new StorageQueryException(e.actualException); } @@ -957,25 +964,23 @@ public void deleteEmailVerificationUserInfo(AppIdentifier appIdentifier, String @Override public void addEmailVerificationToken(AppIdentifier appIdentifier, EmailVerificationTokenInfo emailVerificationInfo) - throws StorageQueryException, DuplicateEmailVerificationTokenException { + throws StorageQueryException, DuplicateEmailVerificationTokenException, TenantOrAppNotFoundException { try { - // TODO.. - EmailVerificationQueries.addEmailVerificationToken(this, emailVerificationInfo.userId, + EmailVerificationQueries.addEmailVerificationToken(this, appIdentifier, emailVerificationInfo.userId, emailVerificationInfo.token, emailVerificationInfo.tokenExpiry, emailVerificationInfo.email); } catch (SQLException e) { - if (e instanceof PSQLException && isPrimaryKeyError(((PSQLException) e).getServerErrorMessage(), - Config.getConfig(this).getEmailVerificationTokensTable())) { - throw new DuplicateEmailVerificationTokenException(); - } + if (e instanceof PSQLException) { + PostgreSQLConfig config = Config.getConfig(this); + ServerErrorMessage serverMessage = ((PSQLException) e).getServerErrorMessage(); - // We keep the old exception detection logic to ensure backwards compatibility. - // We could get here if the new logic hits a false negative, - // e.g., in case someone renamed constraints/tables - if (e.getMessage().contains("ERROR: duplicate key") - && e.getMessage().contains("Key (user_id, email, token)")) { - throw new DuplicateEmailVerificationTokenException(); + if (isPrimaryKeyError(serverMessage, config.getEmailVerificationTokensTable())) { + throw new DuplicateEmailVerificationTokenException(); + } + + if (isForeignKeyConstraintError(serverMessage, config.getEmailVerificationTokensTable(), "app_id")) { + throw new TenantOrAppNotFoundException(appIdentifier); + } } - throw new StorageQueryException(e); } } @@ -983,8 +988,7 @@ public void addEmailVerificationToken(AppIdentifier appIdentifier, EmailVerifica public EmailVerificationTokenInfo getEmailVerificationTokenInfo(AppIdentifier appIdentifier, String token) throws StorageQueryException { try { - // TODO.. - return EmailVerificationQueries.getEmailVerificationTokenInfo(this, token); + return EmailVerificationQueries.getEmailVerificationTokenInfo(this, appIdentifier, token); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -993,8 +997,7 @@ public EmailVerificationTokenInfo getEmailVerificationTokenInfo(AppIdentifier ap @Override public void revokeAllTokens(AppIdentifier appIdentifier, String userId, String email) throws StorageQueryException { try { - // TODO.. - EmailVerificationQueries.revokeAllTokens(this, userId, email); + EmailVerificationQueries.revokeAllTokens(this, appIdentifier, userId, email); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1003,8 +1006,7 @@ public void revokeAllTokens(AppIdentifier appIdentifier, String userId, String e @Override public void unverifyEmail(AppIdentifier appIdentifier, String userId, String email) throws StorageQueryException { try { - // TODO.. - EmailVerificationQueries.unverifyEmail(this, userId, email); + EmailVerificationQueries.unverifyEmail(this, appIdentifier, userId, email); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1014,9 +1016,8 @@ public void unverifyEmail(AppIdentifier appIdentifier, String userId, String ema public EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUser(AppIdentifier appIdentifier, String userId, String email) throws StorageQueryException { - // TODO.. try { - return EmailVerificationQueries.getAllEmailVerificationTokenInfoForUser(this, userId, email); + return EmailVerificationQueries.getAllEmailVerificationTokenInfoForUser(this, appIdentifier, userId, email); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1026,8 +1027,7 @@ public EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUser(AppI public boolean isEmailVerified(AppIdentifier appIdentifier, String userId, String email) throws StorageQueryException { try { - // TODO.. - return EmailVerificationQueries.isEmailVerified(this, userId, email); + return EmailVerificationQueries.isEmailVerified(this, appIdentifier, userId, email); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1050,8 +1050,8 @@ public io.supertokens.pluginInterface.thirdparty.UserInfo getUserInfoUsingId_Tra throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { - // TODO.. - return ThirdPartyQueries.getUserInfoUsingId_Transaction(this, sqlCon, thirdPartyId, thirdPartyUserId); + return ThirdPartyQueries.getUserInfoUsingId_Transaction(this, sqlCon, tenantIdentifier, thirdPartyId, + thirdPartyUserId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1062,9 +1062,9 @@ public void updateUserEmail_Transaction(TenantIdentifier tenantIdentifier, Trans String thirdPartyId, String thirdPartyUserId, String newEmail) throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); - // TODO.. try { - ThirdPartyQueries.updateUserEmail_Transaction(this, sqlCon, thirdPartyId, thirdPartyUserId, newEmail); + ThirdPartyQueries.updateUserEmail_Transaction(this, sqlCon, tenantIdentifier, thirdPartyId, + thirdPartyUserId, newEmail); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1073,29 +1073,33 @@ public void updateUserEmail_Transaction(TenantIdentifier tenantIdentifier, Trans @Override public void signUp(TenantIdentifier tenantIdentifier, io.supertokens.pluginInterface.thirdparty.UserInfo userInfo) throws StorageQueryException, io.supertokens.pluginInterface.thirdparty.exception.DuplicateUserIdException, - DuplicateThirdPartyUserException { - // TODO.. + DuplicateThirdPartyUserException, TenantOrAppNotFoundException { try { - ThirdPartyQueries.signUp(this, userInfo); + ThirdPartyQueries.signUp(this, tenantIdentifier, userInfo); } catch (StorageTransactionLogicException eTemp) { Exception e = eTemp.actualException; if (e instanceof PSQLException) { + PostgreSQLConfig config = Config.getConfig(this); ServerErrorMessage serverMessage = ((PSQLException) e).getServerErrorMessage(); - if (isPrimaryKeyError(serverMessage, Config.getConfig(this).getThirdPartyUsersTable())) { + + if (isUniqueConstraintError(serverMessage, config.getThirdPartyUserToTenantTable(), "third_party_user_id")) { throw new DuplicateThirdPartyUserException(); - } else if (isPrimaryKeyError(serverMessage, Config.getConfig(this).getUsersTable())) { + + } else if (isPrimaryKeyError(serverMessage, config.getThirdPartyUsersTable()) + || isPrimaryKeyError(serverMessage, config.getUsersTable()) + || isPrimaryKeyError(serverMessage, config.getThirdPartyUserToTenantTable()) + || isPrimaryKeyError(serverMessage, config.getAppIdToUserIdTable())) { throw new io.supertokens.pluginInterface.thirdparty.exception.DuplicateUserIdException(); + + } else if (isForeignKeyConstraintError(serverMessage, config.getAppIdToUserIdTable(), "app_id")) { + throw new TenantOrAppNotFoundException(tenantIdentifier.toAppIdentifier()); + + } else if (isForeignKeyConstraintError(serverMessage, config.getUsersTable(), "tenant_id")) { + throw new TenantOrAppNotFoundException(tenantIdentifier); + } - } - // We keep the old exception detection logic to ensure backwards compatibility. - // We could get here if the new logic hits a false negative, - // e.g., in case someone renamed constraints/tables - if (e.getMessage().contains("ERROR: duplicate key") - && e.getMessage().contains("Key (third_party_id, third_party_user_id)")) { - throw new DuplicateThirdPartyUserException(); - } else if (e.getMessage().contains("ERROR: duplicate key") && e.getMessage().contains("Key (user_id)")) { - throw new io.supertokens.pluginInterface.thirdparty.exception.DuplicateUserIdException(); + } throw new StorageQueryException(eTemp.actualException); @@ -1105,8 +1109,7 @@ public void signUp(TenantIdentifier tenantIdentifier, io.supertokens.pluginInter @Override public void deleteThirdPartyUser(AppIdentifier appIdentifier, String userId) throws StorageQueryException { try { - // TODO.. - ThirdPartyQueries.deleteUser(this, userId); + ThirdPartyQueries.deleteUser(this, appIdentifier, userId); } catch (StorageTransactionLogicException e) { throw new StorageQueryException(e.actualException); } @@ -1117,9 +1120,8 @@ public io.supertokens.pluginInterface.thirdparty.UserInfo getThirdPartyUserInfoU TenantIdentifier tenantIdentifier, String thirdPartyId, String thirdPartyUserId) throws StorageQueryException { - // TODO.. try { - return ThirdPartyQueries.getThirdPartyUserInfoUsingId(this, thirdPartyId, thirdPartyUserId); + return ThirdPartyQueries.getThirdPartyUserInfoUsingId(this, tenantIdentifier, thirdPartyId, thirdPartyUserId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1129,9 +1131,8 @@ public io.supertokens.pluginInterface.thirdparty.UserInfo getThirdPartyUserInfoU public io.supertokens.pluginInterface.thirdparty.UserInfo getThirdPartyUserInfoUsingId(AppIdentifier appIdentifier, String id) throws StorageQueryException { - // TODO.. try { - return ThirdPartyQueries.getThirdPartyUserInfoUsingId(this, id); + return ThirdPartyQueries.getThirdPartyUserInfoUsingId(this, appIdentifier, id); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1141,9 +1142,8 @@ public io.supertokens.pluginInterface.thirdparty.UserInfo getThirdPartyUserInfoU public io.supertokens.pluginInterface.thirdparty.UserInfo[] getThirdPartyUsersByEmail( TenantIdentifier tenantIdentifier, @NotNull String email) throws StorageQueryException { - // TODO.. try { - return ThirdPartyQueries.getThirdPartyUsersByEmail(this, email); + return ThirdPartyQueries.getThirdPartyUsersByEmail(this, tenantIdentifier, email); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1250,10 +1250,9 @@ private boolean isPrimaryKeyError(ServerErrorMessage serverMessage, String table public PasswordlessDevice getDevice_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, String deviceIdHash) throws StorageQueryException { - // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { - return PasswordlessQueries.getDevice_Transaction(this, sqlCon, deviceIdHash); + return PasswordlessQueries.getDevice_Transaction(this, sqlCon, tenantIdentifier, deviceIdHash); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1263,10 +1262,9 @@ public PasswordlessDevice getDevice_Transaction(TenantIdentifier tenantIdentifie public void incrementDeviceFailedAttemptCount_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, String deviceIdHash) throws StorageQueryException { - // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { - PasswordlessQueries.incrementDeviceFailedAttemptCount_Transaction(this, sqlCon, deviceIdHash); + PasswordlessQueries.incrementDeviceFailedAttemptCount_Transaction(this, sqlCon, tenantIdentifier, deviceIdHash); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1277,10 +1275,9 @@ public void incrementDeviceFailedAttemptCount_Transaction(TenantIdentifier tenan public PasswordlessCode[] getCodesOfDevice_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, String deviceIdHash) throws StorageQueryException { - // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { - return PasswordlessQueries.getCodesOfDevice_Transaction(this, sqlCon, deviceIdHash); + return PasswordlessQueries.getCodesOfDevice_Transaction(this, sqlCon, tenantIdentifier, deviceIdHash); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1289,10 +1286,9 @@ public PasswordlessCode[] getCodesOfDevice_Transaction(TenantIdentifier tenantId @Override public void deleteDevice_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, String deviceIdHash) throws StorageQueryException { - // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { - PasswordlessQueries.deleteDevice_Transaction(this, sqlCon, deviceIdHash); + PasswordlessQueries.deleteDevice_Transaction(this, sqlCon, tenantIdentifier, deviceIdHash); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1303,10 +1299,9 @@ public void deleteDevice_Transaction(TenantIdentifier tenantIdentifier, Transact public void deleteDevicesByPhoneNumber_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, @Nonnull String phoneNumber) throws StorageQueryException { - // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { - PasswordlessQueries.deleteDevicesByPhoneNumber_Transaction(this, sqlCon, phoneNumber); + PasswordlessQueries.deleteDevicesByPhoneNumber_Transaction(this, sqlCon, tenantIdentifier, phoneNumber); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1316,22 +1311,21 @@ public void deleteDevicesByPhoneNumber_Transaction(TenantIdentifier tenantIdenti public void deleteDevicesByEmail_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, @Nonnull String email) throws StorageQueryException { - // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { - PasswordlessQueries.deleteDevicesByEmail_Transaction(this, sqlCon, email); + PasswordlessQueries.deleteDevicesByEmail_Transaction(this, sqlCon, tenantIdentifier, email); } catch (SQLException e) { throw new StorageQueryException(e); } } + @Override public void deleteDevicesByPhoneNumber_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String phoneNumber, String userId) throws StorageQueryException { - // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { - PasswordlessQueries.deleteDevicesByPhoneNumber_Transaction(this, sqlCon, phoneNumber); + PasswordlessQueries.deleteDevicesByPhoneNumber_Transaction(this, sqlCon, appIdentifier, phoneNumber, userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1340,10 +1334,9 @@ public void deleteDevicesByPhoneNumber_Transaction(AppIdentifier appIdentifier, @Override public void deleteDevicesByEmail_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String email, String userId) throws StorageQueryException { - // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { - PasswordlessQueries.deleteDevicesByEmail_Transaction(this, sqlCon, email); + PasswordlessQueries.deleteDevicesByEmail_Transaction(this, sqlCon, appIdentifier, email, userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1353,10 +1346,9 @@ public void deleteDevicesByEmail_Transaction(AppIdentifier appIdentifier, Transa public PasswordlessCode getCodeByLinkCodeHash_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, String linkCodeHash) throws StorageQueryException { - // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { - return PasswordlessQueries.getCodeByLinkCodeHash_Transaction(this, sqlCon, linkCodeHash); + return PasswordlessQueries.getCodeByLinkCodeHash_Transaction(this, sqlCon, tenantIdentifier, linkCodeHash); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1365,10 +1357,9 @@ public PasswordlessCode getCodeByLinkCodeHash_Transaction(TenantIdentifier tenan @Override public void deleteCode_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, String deviceIdHash) throws StorageQueryException { - // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { - PasswordlessQueries.deleteCode_Transaction(this, sqlCon, deviceIdHash); + PasswordlessQueries.deleteCode_Transaction(this, sqlCon, tenantIdentifier, deviceIdHash); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1378,10 +1369,9 @@ public void deleteCode_Transaction(TenantIdentifier tenantIdentifier, Transactio public void updateUserEmail_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId, String email) throws StorageQueryException, UnknownUserIdException, DuplicateEmailException { - // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { - int updated_rows = PasswordlessQueries.updateUserEmail_Transaction(this, sqlCon, userId, email); + int updated_rows = PasswordlessQueries.updateUserEmail_Transaction(this, sqlCon, appIdentifier, userId, email); if (updated_rows != 1) { throw new UnknownUserIdException(); } @@ -1389,7 +1379,7 @@ public void updateUserEmail_Transaction(AppIdentifier appIdentifier, Transaction if (e instanceof PSQLException) { if (isUniqueConstraintError(((PSQLException) e).getServerErrorMessage(), - Config.getConfig(this).getPasswordlessUsersTable(), "email")) { + Config.getConfig(this).getPasswordlessUserToTenantTable(), "email")) { throw new DuplicateEmailException(); } @@ -1403,10 +1393,9 @@ public void updateUserEmail_Transaction(AppIdentifier appIdentifier, Transaction public void updateUserPhoneNumber_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId, String phoneNumber) throws StorageQueryException, UnknownUserIdException, DuplicatePhoneNumberException { - // TODO... Connection sqlCon = (Connection) con.getConnection(); try { - int updated_rows = PasswordlessQueries.updateUserPhoneNumber_Transaction(this, sqlCon, userId, phoneNumber); + int updated_rows = PasswordlessQueries.updateUserPhoneNumber_Transaction(this, sqlCon, appIdentifier, userId, phoneNumber); if (updated_rows != 1) { throw new UnknownUserIdException(); @@ -1416,7 +1405,7 @@ public void updateUserPhoneNumber_Transaction(AppIdentifier appIdentifier, Trans if (e instanceof PSQLException) { if (isUniqueConstraintError(((PSQLException) e).getServerErrorMessage(), - Config.getConfig(this).getPasswordlessUsersTable(), "phone_number")) { + Config.getConfig(this).getPasswordlessUserToTenantTable(), "phone_number")) { throw new DuplicatePhoneNumberException(); } @@ -1431,13 +1420,12 @@ public void createDeviceWithCode(TenantIdentifier tenantIdentifier, @Nullable St @Nullable String phoneNumber, @NotNull String linkCodeSalt, PasswordlessCode code) throws StorageQueryException, DuplicateDeviceIdHashException, - DuplicateCodeIdException, DuplicateLinkCodeHashException { - // TODO.. + DuplicateCodeIdException, DuplicateLinkCodeHashException, TenantOrAppNotFoundException { if (email == null && phoneNumber == null) { throw new IllegalArgumentException("Both email and phoneNumber can't be null"); } try { - PasswordlessQueries.createDeviceWithCode(this, email, phoneNumber, linkCodeSalt, code); + PasswordlessQueries.createDeviceWithCode(this, tenantIdentifier, email, phoneNumber, linkCodeSalt, code); } catch (StorageTransactionLogicException e) { Exception actualException = e.actualException; @@ -1453,7 +1441,10 @@ public void createDeviceWithCode(TenantIdentifier tenantIdentifier, @Nullable St if (isUniqueConstraintError(((PSQLException) actualException).getServerErrorMessage(), Config.getConfig(this).getPasswordlessCodesTable(), "link_code_hash")) { throw new DuplicateLinkCodeHashException(); - + } + if (isForeignKeyConstraintError(((PSQLException) actualException).getServerErrorMessage(), + Config.getConfig(this).getPasswordlessDevicesTable(), "tenant_id")) { + throw new TenantOrAppNotFoundException(tenantIdentifier); } } @@ -1465,9 +1456,8 @@ public void createDeviceWithCode(TenantIdentifier tenantIdentifier, @Nullable St public void createCode(TenantIdentifier tenantIdentifier, PasswordlessCode code) throws StorageQueryException, UnknownDeviceIdHash, DuplicateCodeIdException, DuplicateLinkCodeHashException { - // TODO.. try { - PasswordlessQueries.createCode(this, code); + PasswordlessQueries.createCode(this, tenantIdentifier, code); } catch (StorageTransactionLogicException e) { Exception actualException = e.actualException; @@ -1484,7 +1474,6 @@ public void createCode(TenantIdentifier tenantIdentifier, PasswordlessCode code) if (isUniqueConstraintError(((PSQLException) actualException).getServerErrorMessage(), Config.getConfig(this).getPasswordlessCodesTable(), "link_code_hash")) { throw new DuplicateLinkCodeHashException(); - } } throw new StorageQueryException(e.actualException); @@ -1494,33 +1483,43 @@ public void createCode(TenantIdentifier tenantIdentifier, PasswordlessCode code) @Override public void createUser(TenantIdentifier tenantIdentifier, io.supertokens.pluginInterface.passwordless.UserInfo user) throws StorageQueryException, - DuplicateEmailException, DuplicatePhoneNumberException, DuplicateUserIdException { + DuplicateEmailException, DuplicatePhoneNumberException, DuplicateUserIdException, + TenantOrAppNotFoundException { try { - // TODO.. - PasswordlessQueries.createUser(this, user); + PasswordlessQueries.createUser(this, tenantIdentifier, user); } catch (StorageTransactionLogicException e) { - String message = e.actualException.getMessage(); Exception actualException = e.actualException; if (actualException instanceof PSQLException) { - if (isPrimaryKeyError(((PSQLException) actualException).getServerErrorMessage(), - Config.getConfig(this).getPasswordlessUsersTable()) - || isPrimaryKeyError(((PSQLException) actualException).getServerErrorMessage(), - Config.getConfig(this).getUsersTable())) { + PostgreSQLConfig config = Config.getConfig(this); + ServerErrorMessage serverMessage = ((PSQLException) actualException).getServerErrorMessage(); + + if (isPrimaryKeyError(serverMessage, config.getPasswordlessUsersTable()) + || isPrimaryKeyError(serverMessage, config.getUsersTable()) + || isPrimaryKeyError(serverMessage, config.getPasswordlessUserToTenantTable()) + || isPrimaryKeyError(serverMessage, config.getAppIdToUserIdTable())) { throw new DuplicateUserIdException(); } if (isUniqueConstraintError(((PSQLException) actualException).getServerErrorMessage(), - Config.getConfig(this).getPasswordlessUsersTable(), "email")) { + Config.getConfig(this).getPasswordlessUserToTenantTable(), "email")) { throw new DuplicateEmailException(); } if (isUniqueConstraintError(((PSQLException) actualException).getServerErrorMessage(), - Config.getConfig(this).getPasswordlessUsersTable(), "phone_number")) { + Config.getConfig(this).getPasswordlessUserToTenantTable(), "phone_number")) { throw new DuplicatePhoneNumberException(); } + if (isForeignKeyConstraintError(serverMessage, config.getAppIdToUserIdTable(), "app_id")) { + throw new TenantOrAppNotFoundException(tenantIdentifier.toAppIdentifier()); + } + + if (isForeignKeyConstraintError(serverMessage, config.getUsersTable(), "tenant_id")) { + throw new TenantOrAppNotFoundException(tenantIdentifier); + } + } throw new StorageQueryException(e.actualException); } @@ -1529,8 +1528,7 @@ public void createUser(TenantIdentifier tenantIdentifier, io.supertokens.pluginI @Override public void deletePasswordlessUser(AppIdentifier appIdentifier, String userId) throws StorageQueryException { try { - // TODO.. - PasswordlessQueries.deleteUser(this, userId); + PasswordlessQueries.deleteUser(this, appIdentifier, userId); } catch (StorageTransactionLogicException e) { throw new StorageQueryException(e.actualException); } @@ -1540,8 +1538,7 @@ public void deletePasswordlessUser(AppIdentifier appIdentifier, String userId) t public PasswordlessDevice getDevice(TenantIdentifier tenantIdentifier, String deviceIdHash) throws StorageQueryException { try { - // TODO.. - return PasswordlessQueries.getDevice(this, deviceIdHash); + return PasswordlessQueries.getDevice(this, tenantIdentifier, deviceIdHash); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1551,8 +1548,7 @@ public PasswordlessDevice getDevice(TenantIdentifier tenantIdentifier, String de public PasswordlessDevice[] getDevicesByEmail(TenantIdentifier tenantIdentifier, String email) throws StorageQueryException { try { - // TODO.. - return PasswordlessQueries.getDevicesByEmail(this, email); + return PasswordlessQueries.getDevicesByEmail(this, tenantIdentifier, email); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1562,8 +1558,7 @@ public PasswordlessDevice[] getDevicesByEmail(TenantIdentifier tenantIdentifier, public PasswordlessDevice[] getDevicesByPhoneNumber(TenantIdentifier tenantIdentifier, String phoneNumber) throws StorageQueryException { try { - // TODO.. - return PasswordlessQueries.getDevicesByPhoneNumber(this, phoneNumber); + return PasswordlessQueries.getDevicesByPhoneNumber(this, tenantIdentifier, phoneNumber); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1573,8 +1568,7 @@ public PasswordlessDevice[] getDevicesByPhoneNumber(TenantIdentifier tenantIdent public PasswordlessCode[] getCodesOfDevice(TenantIdentifier tenantIdentifier, String deviceIdHash) throws StorageQueryException { try { - // TODO.. - return PasswordlessQueries.getCodesOfDevice(this, deviceIdHash); + return PasswordlessQueries.getCodesOfDevice(this, tenantIdentifier, deviceIdHash); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1584,8 +1578,7 @@ public PasswordlessCode[] getCodesOfDevice(TenantIdentifier tenantIdentifier, St public PasswordlessCode[] getCodesBefore(TenantIdentifier tenantIdentifier, long time) throws StorageQueryException { try { - // TODO.. - return PasswordlessQueries.getCodesBefore(this, time); + return PasswordlessQueries.getCodesBefore(this, tenantIdentifier, time); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1594,8 +1587,7 @@ public PasswordlessCode[] getCodesBefore(TenantIdentifier tenantIdentifier, long @Override public PasswordlessCode getCode(TenantIdentifier tenantIdentifier, String codeId) throws StorageQueryException { try { - // TODO.. - return PasswordlessQueries.getCode(this, codeId); + return PasswordlessQueries.getCode(this, tenantIdentifier, codeId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1605,8 +1597,7 @@ public PasswordlessCode getCode(TenantIdentifier tenantIdentifier, String codeId public PasswordlessCode getCodeByLinkCodeHash(TenantIdentifier tenantIdentifier, String linkCodeHash) throws StorageQueryException { try { - // TODO.. - return PasswordlessQueries.getCodeByLinkCodeHash(this, linkCodeHash); + return PasswordlessQueries.getCodeByLinkCodeHash(this, tenantIdentifier, linkCodeHash); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1616,9 +1607,8 @@ public PasswordlessCode getCodeByLinkCodeHash(TenantIdentifier tenantIdentifier, public io.supertokens.pluginInterface.passwordless.UserInfo getUserById(AppIdentifier appIdentifier, String userId) throws StorageQueryException { - // TODO.. try { - return PasswordlessQueries.getUserById(this, userId); + return PasswordlessQueries.getUserById(this, appIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1628,9 +1618,8 @@ public io.supertokens.pluginInterface.passwordless.UserInfo getUserById(AppIdent public io.supertokens.pluginInterface.passwordless.UserInfo getUserByEmail(TenantIdentifier tenantIdentifier, String email) throws StorageQueryException { - // TODO.. try { - return PasswordlessQueries.getUserByEmail(this, email); + return PasswordlessQueries.getUserByEmail(this, tenantIdentifier, email); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1640,9 +1629,8 @@ public io.supertokens.pluginInterface.passwordless.UserInfo getUserByEmail(Tenan public io.supertokens.pluginInterface.passwordless.UserInfo getUserByPhoneNumber(TenantIdentifier tenantIdentifier, String phoneNumber) throws StorageQueryException { - // TODO... try { - return PasswordlessQueries.getUserByPhoneNumber(this, phoneNumber); + return PasswordlessQueries.getUserByPhoneNumber(this, tenantIdentifier, phoneNumber); } catch (SQLException e) { throw new StorageQueryException(e); } diff --git a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java index df7cd0a0..265c400b 100644 --- a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java +++ b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java @@ -310,11 +310,21 @@ public String getThirdPartyUsersTable() { return addSchemaAndPrefixToTableName(tableName); } + public String getThirdPartyUserToTenantTable() { + String tableName = "thirdparty_user_to_tenant"; + return addSchemaAndPrefixToTableName(tableName); + } + public String getPasswordlessUsersTable() { String tableName = "passwordless_users"; return addSchemaAndPrefixToTableName(tableName); } + public String getPasswordlessUserToTenantTable() { + String tableName = "passwordless_user_to_tenant"; + return addSchemaAndPrefixToTableName(tableName); + } + public String getPasswordlessDevicesTable() { String tableName = "passwordless_devices"; return addSchemaAndPrefixToTableName(tableName); diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java index 3c084e20..87aa6d18 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java @@ -345,17 +345,13 @@ public static UserInfo getUserInfoUsingId(Start start, AppIdentifier appIdentifi }); } - public static List getUsersInfoUsingIdList(Start start, TenantIdentifier tenantIdentifier, List ids) + public static List getUsersInfoUsingIdList(Start start, List ids) throws SQLException, StorageQueryException { if (ids.size() > 0) { - StringBuilder QUERY = new StringBuilder( - "SELECT ep_users.user_id as user_id, ep_users.email as email, ep_users.password_hash as password_hash, " - + "ep_users.time_joined as time_joined, ep_users_to_tenant.app_id, ep_users_to_tenant.tenant_id, ep_users_to_tenant.user_id " - + "FROM " + getConfig(start).getEmailPasswordUsersTable() + " AS ep_users " - + "JOIN " + getConfig(start).getEmailPasswordUserToTenantTable() + " AS ep_users_to_tenant " - + "ON ep_users.app_id = ep_users_to_tenant.app_id AND ep_users.user_id = ep_users_to_tenant.user_id" - ); - QUERY.append(" WHERE ep_users_to_tenant.app_id = ? AND ep_users_to_tenant.tenant_id = ? AND ep_users_to_tenant.user_id IN ("); + // No need to filter based on tenantId because the id list is already filtered for a tenant + StringBuilder QUERY = new StringBuilder("SELECT user_id, email, password_hash, time_joined " + + "FROM " + getConfig(start).getEmailPasswordUsersTable()); + QUERY.append(" WHERE user_id IN ("); for (int i = 0; i < ids.size(); i++) { QUERY.append("?"); @@ -367,11 +363,9 @@ public static List getUsersInfoUsingIdList(Start start, TenantIdentifi QUERY.append(")"); return execute(start, QUERY.toString(), pst -> { - pst.setString(1, tenantIdentifier.getAppId()); - pst.setString(2, tenantIdentifier.getTenantId()); for (int i = 0; i < ids.size(); i++) { - // i+3 cause this starts with 1 and not 0, and 1 is used for app_id, 2 is used for tenant_id - pst.setString(i + 3, ids.get(i)); + // i+1 cause this starts with 1 and not 0 + pst.setString(i + 1, ids.get(i)); } }, result -> { List finalResult = new ArrayList<>(); @@ -385,42 +379,23 @@ public static List getUsersInfoUsingIdList(Start start, TenantIdentifi } public static UserInfo getUserInfoUsingEmail(Start start, TenantIdentifier tenantIdentifier, String email) throws StorageQueryException, SQLException { - String userId = null; - { - // check if user exists for the provided tenant - String QUERY = "SELECT user_id FROM " - + getConfig(start).getEmailPasswordUserToTenantTable() - + " WHERE app_id = ? AND tenant_id = ? AND email = ?"; - - userId = execute(start, QUERY, pst -> { - pst.setString(1, tenantIdentifier.getAppId()); - pst.setString(2, tenantIdentifier.getTenantId()); - pst.setString(3, email); - }, result -> { - if (result.next()) { - return result.getString("user_id"); - } - return null; - }); + String QUERY = "SELECT ep_users_to_tenant.user_id as user_id, ep_users_to_tenant.email as email, " + + "ep_users.password_hash as password_hash, ep_users.time_joined as time_joined " + + "FROM " + getConfig(start).getEmailPasswordUserToTenantTable() + " AS ep_users_to_tenant " + + "JOIN " + getConfig(start).getEmailPasswordUsersTable() + " AS ep_users " + + "ON ep_users.app_id = ep_users_to_tenant.app_id AND ep_users.user_id = ep_users_to_tenant.user_id " + + "WHERE ep_users_to_tenant.app_id = ? AND ep_users_to_tenant.tenant_id = ? AND ep_users_to_tenant.email = ?"; - if (userId == null) { - return null; + return execute(start, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, email); + }, result -> { + if (result.next()) { + return UserInfoRowMapper.getInstance().mapOrThrow(result); } - } - { - String QUERY = "SELECT user_id, email, password_hash, time_joined FROM " - + getConfig(start).getEmailPasswordUsersTable() + " WHERE app_id = ? AND user_id = ?"; - final String userIdToQuery = userId; - return execute(start, QUERY, pst -> { - pst.setString(1, tenantIdentifier.getAppId()); - pst.setString(2, userIdToQuery); - }, result -> { - if (result.next()) { - return UserInfoRowMapper.getInstance().mapOrThrow(result); - } - return null; - }); - } + return null; + }); } private static class PasswordResetRowMapper implements RowMapper { diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/EmailVerificationQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/EmailVerificationQueries.java index d0b11c3f..aee32bc1 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/EmailVerificationQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/EmailVerificationQueries.java @@ -20,6 +20,7 @@ import io.supertokens.pluginInterface.emailverification.EmailVerificationTokenInfo; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.config.Config; import io.supertokens.storage.postgresql.utils.Utils; @@ -42,9 +43,15 @@ static String getQueryToCreateEmailVerificationTable(Start start) { String emailVerificationTable = Config.getConfig(start).getEmailVerificationTable(); // @formatter:off return "CREATE TABLE IF NOT EXISTS " + emailVerificationTable + " (" + + "app_id VARCHAR(64) DEFAULT 'public'," + "user_id VARCHAR(128) NOT NULL," + "email VARCHAR(256) NOT NULL," - + "CONSTRAINT " + Utils.getConstraintName(schema, emailVerificationTable, null, "pkey") + " PRIMARY KEY (user_id, email));"; + + "CONSTRAINT " + Utils.getConstraintName(schema, emailVerificationTable, null, "pkey") + + " PRIMARY KEY (app_id, user_id, email)," + + "CONSTRAINT " + Utils.getConstraintName(schema, emailVerificationTable, "app_id", "fkey") + + " FOREIGN KEY(app_id)" + + " REFERENCES " + Config.getConfig(start).getAppsTable() + " (app_id) ON DELETE CASCADE" + + ");"; // @formatter:on } @@ -53,12 +60,17 @@ static String getQueryToCreateEmailVerificationTokensTable(Start start) { String emailVerificationTokensTable = Config.getConfig(start).getEmailVerificationTokensTable(); // @formatter:off return "CREATE TABLE IF NOT EXISTS " + emailVerificationTokensTable + " (" + + "app_id VARCHAR(64) DEFAULT 'public'," + "user_id VARCHAR(128) NOT NULL," + "email VARCHAR(256) NOT NULL," + "token VARCHAR(128) NOT NULL CONSTRAINT " + Utils.getConstraintName(schema, emailVerificationTokensTable, "token", "key") + " UNIQUE," + "token_expiry BIGINT NOT NULL," - + "CONSTRAINT " + Utils.getConstraintName(schema, emailVerificationTokensTable, null, "pkey") + - " PRIMARY KEY (user_id, email, token))"; + + "CONSTRAINT " + Utils.getConstraintName(schema, emailVerificationTokensTable, null, "pkey") + + " PRIMARY KEY (app_id, user_id, email, token), " + + "CONSTRAINT " + Utils.getConstraintName(schema, emailVerificationTokensTable, "app_id", "fkey") + + " FOREIGN KEY(app_id)" + + " REFERENCES " + Config.getConfig(start).getAppsTable() + " (app_id) ON DELETE CASCADE" + + ")"; // @formatter:on } @@ -73,44 +85,53 @@ public static void deleteExpiredEmailVerificationTokens(Start start) throws SQLE update(start, QUERY, pst -> pst.setLong(1, currentTimeMillis())); } - public static void updateUsersIsEmailVerified_Transaction(Start start, Connection con, String userId, String email, - boolean isEmailVerified) throws SQLException, StorageQueryException { + public static void updateUsersIsEmailVerified_Transaction(Start start, Connection con, AppIdentifier appIdentifier, + String userId, String email, + boolean isEmailVerified) throws SQLException, StorageQueryException { if (isEmailVerified) { String QUERY = "INSERT INTO " + getConfig(start).getEmailVerificationTable() - + "(user_id, email) VALUES(?, ?)"; + + "(app_id, user_id, email) VALUES(?, ?, ?)"; update(con, QUERY, pst -> { - pst.setString(1, userId); - pst.setString(2, email); + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + pst.setString(3, email); }); } else { String QUERY = "DELETE FROM " + getConfig(start).getEmailVerificationTable() - + " WHERE user_id = ? AND email = ?"; + + " WHERE app_id = ? AND user_id = ? AND email = ?"; update(con, QUERY, pst -> { - pst.setString(1, userId); - pst.setString(2, email); + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + pst.setString(3, email); }); } } - public static void deleteAllEmailVerificationTokensForUser_Transaction(Start start, Connection con, String userId, - String email) throws SQLException, StorageQueryException { + public static void deleteAllEmailVerificationTokensForUser_Transaction(Start start, Connection con, + AppIdentifier appIdentifier, String userId, + String email) throws SQLException, StorageQueryException { String QUERY = "DELETE FROM " + getConfig(start).getEmailVerificationTokensTable() - + " WHERE user_id = ? AND email = ?"; + + " WHERE app_id = ? AND user_id = ? AND email = ?"; update(con, QUERY, pst -> { - pst.setString(1, userId); - pst.setString(2, email); + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + pst.setString(3, email); }); } - public static EmailVerificationTokenInfo getEmailVerificationTokenInfo(Start start, String token) + public static EmailVerificationTokenInfo getEmailVerificationTokenInfo(Start start, AppIdentifier appIdentifier, + String token) throws SQLException, StorageQueryException { String QUERY = "SELECT user_id, token, token_expiry, email FROM " - + getConfig(start).getEmailVerificationTokensTable() + " WHERE token = ?"; - return execute(start, QUERY, pst -> pst.setString(1, token), result -> { + + getConfig(start).getEmailVerificationTokensTable() + " WHERE app_id = ? AND token = ?"; + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, token); + }, result -> { if (result.next()) { return EmailVerificationTokenInfoRowMapper.getInstance().mapOrThrow(result); } @@ -118,28 +139,32 @@ public static EmailVerificationTokenInfo getEmailVerificationTokenInfo(Start sta }); } - public static void addEmailVerificationToken(Start start, String userId, String tokenHash, long expiry, - String email) throws SQLException, StorageQueryException { + public static void addEmailVerificationToken(Start start, AppIdentifier appIdentifier, String userId, String tokenHash, long expiry, + String email) throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + getConfig(start).getEmailVerificationTokensTable() - + "(user_id, token, token_expiry, email)" + " VALUES(?, ?, ?, ?)"; + + "(app_id, user_id, token, token_expiry, email)" + " VALUES(?, ?, ?, ?, ?)"; update(start, QUERY, pst -> { - pst.setString(1, userId); - pst.setString(2, tokenHash); - pst.setLong(3, expiry); - pst.setString(4, email); + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + pst.setString(3, tokenHash); + pst.setLong(4, expiry); + pst.setString(5, email); }); } public static EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUser_Transaction(Start start, - Connection con, String userId, String email) throws SQLException, StorageQueryException { + Connection con, + AppIdentifier appIdentifier, + String userId, String email) throws SQLException, StorageQueryException { String QUERY = "SELECT user_id, token, token_expiry, email FROM " - + getConfig(start).getEmailVerificationTokensTable() + " WHERE user_id = ? AND email = ? FOR UPDATE"; + + getConfig(start).getEmailVerificationTokensTable() + " WHERE app_id = ? AND user_id = ? AND email = ? FOR UPDATE"; return execute(con, QUERY, pst -> { - pst.setString(1, userId); - pst.setString(2, email); + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + pst.setString(3, email); }, result -> { List temp = new ArrayList<>(); while (result.next()) { @@ -153,14 +178,17 @@ public static EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUs }); } - public static EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUser(Start start, String userId, - String email) throws SQLException, StorageQueryException { + public static EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUser(Start start, + AppIdentifier appIdentifier, + String userId, + String email) throws SQLException, StorageQueryException { String QUERY = "SELECT user_id, token, token_expiry, email FROM " - + getConfig(start).getEmailVerificationTokensTable() + " WHERE user_id = ? AND email = ?"; + + getConfig(start).getEmailVerificationTokensTable() + " WHERE app_id = ? AND user_id = ? AND email = ?"; return execute(start, QUERY, pst -> { - pst.setString(1, userId); - pst.setString(2, email); + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + pst.setString(3, email); }, result -> { List temp = new ArrayList<>(); while (result.next()) { @@ -174,32 +202,40 @@ public static EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUs }); } - public static boolean isEmailVerified(Start start, String userId, String email) + public static boolean isEmailVerified(Start start, AppIdentifier appIdentifier, String userId, String email) throws SQLException, StorageQueryException { String QUERY = "SELECT * FROM " + getConfig(start).getEmailVerificationTable() - + " WHERE user_id = ? AND email = ?"; + + " WHERE app_id = ? AND user_id = ? AND email = ?"; return execute(start, QUERY, pst -> { - pst.setString(1, userId); - pst.setString(2, email); + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + pst.setString(3, email); }, result -> result.next()); } - public static void deleteUserInfo(Start start, String userId) + public static void deleteUserInfo(Start start, AppIdentifier appIdentifier, String userId) throws StorageQueryException, StorageTransactionLogicException { start.startTransaction(con -> { Connection sqlCon = (Connection) con.getConnection(); try { { - String QUERY = "DELETE FROM " + getConfig(start).getEmailVerificationTable() + " WHERE user_id = ?"; - update(sqlCon, QUERY, pst -> pst.setString(1, userId)); + String QUERY = "DELETE FROM " + getConfig(start).getEmailVerificationTable() + + " WHERE app_id = ? AND user_id = ?"; + update(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); } { String QUERY = "DELETE FROM " + getConfig(start).getEmailVerificationTokensTable() - + " WHERE user_id = ?"; + + " WHERE app_id = ? AND user_id = ?"; - update(sqlCon, QUERY, pst -> pst.setString(1, userId)); + update(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); } sqlCon.commit(); @@ -210,34 +246,38 @@ public static void deleteUserInfo(Start start, String userId) }); } - public static void unverifyEmail(Start start, String userId, String email) + public static void unverifyEmail(Start start, AppIdentifier appIdentifier, String userId, String email) throws SQLException, StorageQueryException { String QUERY = "DELETE FROM " + getConfig(start).getEmailVerificationTable() - + " WHERE user_id = ? AND email = ?"; + + " WHERE app_id = ? AND user_id = ? AND email = ?"; update(start, QUERY, pst -> { - pst.setString(1, userId); - pst.setString(2, email); + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + pst.setString(3, email); }); } - public static void revokeAllTokens(Start start, String userId, String email) + public static void revokeAllTokens(Start start, AppIdentifier appIdentifier, String userId, String email) throws SQLException, StorageQueryException { String QUERY = "DELETE FROM " + getConfig(start).getEmailVerificationTokensTable() - + " WHERE user_id = ? AND email = ?"; + + " WHERE app_id = ? AND user_id = ? AND email = ?"; update(start, QUERY, pst -> { - pst.setString(1, userId); - pst.setString(2, email); + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + pst.setString(3, email); }); } - public static boolean isUserIdBeingUsedForEmailVerification(Start start, String userId) + public static boolean isUserIdBeingUsedForEmailVerification(Start start, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { - String QUERY = "SELECT * FROM " + getConfig(start).getEmailVerificationTokensTable() + " WHERE user_id = ?"; + String QUERY = "SELECT * FROM " + getConfig(start).getEmailVerificationTokensTable() + + " WHERE app_id = ? AND user_id = ?"; return execute(start, QUERY, pst -> { - pst.setString(1, userId); + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); }, ResultSet::next); } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java index 70653544..b6321809 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java @@ -21,6 +21,7 @@ import io.supertokens.pluginInterface.RowMapper; import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.storage.postgresql.ConnectionPool; @@ -244,6 +245,13 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto if (!doesTableExists(start, Config.getConfig(start).getThirdPartyUsersTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); update(start, ThirdPartyQueries.getQueryToCreateUsersTable(start), NO_OP_SETTER); + // index + update(start, ThirdPartyQueries.getQueryToThirdPartyUserEmailIndex(start), NO_OP_SETTER); + } + + if (!doesTableExists(start, Config.getConfig(start).getThirdPartyUserToTenantTable())) { + getInstance(start).addState(CREATING_NEW_TABLE, null); + update(start, ThirdPartyQueries.getQueryToCreateThirdPartyUserToTenantTable(start), NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getJWTSigningKeysTable())) { @@ -256,6 +264,11 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto update(start, PasswordlessQueries.getQueryToCreateUsersTable(start), NO_OP_SETTER); } + if (!doesTableExists(start, Config.getConfig(start).getPasswordlessUserToTenantTable())) { + getInstance(start).addState(CREATING_NEW_TABLE, null); + update(start, PasswordlessQueries.getQueryToCreatePasswordlessUserToTenantTable(start), NO_OP_SETTER); + } + if (!doesTableExists(start, Config.getConfig(start).getPasswordlessDevicesTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); update(start, getQueryToCreateDevicesTable(start), NO_OP_SETTER); @@ -372,9 +385,11 @@ public static void deleteAllTables(Start start) throws SQLException, StorageQuer + getConfig(start).getEmailVerificationTokensTable() + "," + getConfig(start).getEmailVerificationTable() + "," + getConfig(start).getThirdPartyUsersTable() + "," + + getConfig(start).getThirdPartyUserToTenantTable() + "," + getConfig(start).getJWTSigningKeysTable() + "," + getConfig(start).getPasswordlessCodesTable() + "," + getConfig(start).getPasswordlessDevicesTable() + "," + + getConfig(start).getPasswordlessUserToTenantTable() + "," + getConfig(start).getPasswordlessUsersTable() + "," + getConfig(start).getUserMetadataTable() + "," + getConfig(start).getRolesTable() + "," @@ -576,11 +591,11 @@ private static List getUserInfoForRecipeIdFromUser List userIds) throws StorageQueryException, SQLException { if (recipeId == RECIPE_ID.EMAIL_PASSWORD) { - return EmailPasswordQueries.getUsersInfoUsingIdList(start, tenantIdentifier, userIds); + return EmailPasswordQueries.getUsersInfoUsingIdList(start, userIds); } else if (recipeId == RECIPE_ID.THIRD_PARTY) { - return ThirdPartyQueries.getUsersInfoUsingIdList(start, userIds); // TODO pass tenantIdentifier + return ThirdPartyQueries.getUsersInfoUsingIdList(start, userIds); } else if (recipeId == RECIPE_ID.PASSWORDLESS) { - return PasswordlessQueries.getUsersByIdList(start, userIds); // TODO pass tenantIdentifier + return PasswordlessQueries.getUsersByIdList(start, userIds); } else { throw new IllegalArgumentException("No implementation of get users for recipe: " + recipeId.toString()); } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java index 586adbff..f842271c 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java @@ -19,6 +19,8 @@ import io.supertokens.pluginInterface.RowMapper; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.passwordless.PasswordlessCode; import io.supertokens.pluginInterface.passwordless.PasswordlessDevice; import io.supertokens.pluginInterface.passwordless.UserInfo; @@ -46,75 +48,123 @@ public static String getQueryToCreateUsersTable(Start start) { String schema = Config.getConfig(start).getTableSchema(); String usersTable = Config.getConfig(start).getPasswordlessUsersTable(); - return "CREATE TABLE IF NOT EXISTS " + usersTable + " (" + "user_id CHAR(36) NOT NULL," - + "email VARCHAR(256) CONSTRAINT " + Utils.getConstraintName(schema, usersTable, "email", "key") - + " UNIQUE," + "phone_number VARCHAR(256) CONSTRAINT " - + Utils.getConstraintName(schema, usersTable, "phone_number", "key") + " UNIQUE," - + "time_joined BIGINT NOT NULL, " + "CONSTRAINT " - + Utils.getConstraintName(schema, usersTable, null, "pkey") + " PRIMARY KEY (user_id)" + ");"; + return "CREATE TABLE IF NOT EXISTS " + usersTable + " (" + + "app_id VARCHAR(64) DEFAULT 'public'," + + "user_id CHAR(36) NOT NULL," + + "email VARCHAR(256)," + + "phone_number VARCHAR(256)," + + "time_joined BIGINT NOT NULL, " + + "CONSTRAINT " + Utils.getConstraintName(schema, usersTable, "app_id", "fkey") + + " FOREIGN KEY(app_id, user_id)" + + " REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable() + " (app_id, user_id) ON DELETE CASCADE," + + "CONSTRAINT " + Utils.getConstraintName(schema, usersTable, null, "pkey") + + " PRIMARY KEY (app_id, user_id)" + + ");"; + } + + static String getQueryToCreatePasswordlessUserToTenantTable(Start start) { + String schema = Config.getConfig(start).getTableSchema(); + String passwordlessUserToTenantTable = Config.getConfig(start).getPasswordlessUserToTenantTable(); + // @formatter:off + return "CREATE TABLE IF NOT EXISTS " + passwordlessUserToTenantTable + " (" + + "app_id VARCHAR(64) DEFAULT 'public'," + + "tenant_id VARCHAR(64) DEFAULT 'public'," + + "user_id CHAR(36) NOT NULL," + + "email VARCHAR(256)," + + "phone_number VARCHAR(256)," + + "CONSTRAINT " + Utils.getConstraintName(schema, passwordlessUserToTenantTable, "email", "key") + + " UNIQUE (app_id, tenant_id, email)," + + "CONSTRAINT " + Utils.getConstraintName(schema, passwordlessUserToTenantTable, "phone_number", "key") + + " UNIQUE (app_id, tenant_id, phone_number)," + + "CONSTRAINT " + Utils.getConstraintName(schema, passwordlessUserToTenantTable, null, "pkey") + + " PRIMARY KEY (app_id, tenant_id, user_id)," + + "CONSTRAINT " + Utils.getConstraintName(schema, passwordlessUserToTenantTable, "user_id", "fkey") + + " FOREIGN KEY (app_id, tenant_id, user_id)" + + " REFERENCES " + Config.getConfig(start).getUsersTable() + "(app_id, tenant_id, user_id) ON DELETE CASCADE" + + ");"; + // @formatter:on } public static String getQueryToCreateDevicesTable(Start start) { String schema = Config.getConfig(start).getTableSchema(); String devicesTable = Config.getConfig(start).getPasswordlessDevicesTable(); - return "CREATE TABLE IF NOT EXISTS " + devicesTable + " (" + "device_id_hash CHAR(44) NOT NULL," - + "email VARCHAR(256), " + "phone_number VARCHAR(256)," + "link_code_salt CHAR(44) NOT NULL," - + "failed_attempts INT NOT NULL," + "CONSTRAINT " - + Utils.getConstraintName(schema, devicesTable, null, "pkey") + " PRIMARY KEY (device_id_hash));"; + return "CREATE TABLE IF NOT EXISTS " + devicesTable + " (" + + "app_id VARCHAR(64) DEFAULT 'public'," + + "tenant_id VARCHAR(64) DEFAULT 'public'," + + "device_id_hash CHAR(44) NOT NULL," + + "email VARCHAR(256), " + + "phone_number VARCHAR(256)," + + "link_code_salt CHAR(44) NOT NULL," + + "failed_attempts INT NOT NULL," + + "CONSTRAINT " + Utils.getConstraintName(schema, devicesTable, "tenant_id", "fkey") + + " FOREIGN KEY(app_id, tenant_id)" + + " REFERENCES " + Config.getConfig(start).getTenantsTable() + " (app_id, tenant_id) ON DELETE CASCADE," + + "CONSTRAINT " + Utils.getConstraintName(schema, devicesTable, null, "pkey") + + " PRIMARY KEY (app_id, tenant_id, device_id_hash)" + + ");"; } public static String getQueryToCreateCodesTable(Start start) { String schema = Config.getConfig(start).getTableSchema(); String codesTable = Config.getConfig(start).getPasswordlessCodesTable(); - return "CREATE TABLE IF NOT EXISTS " + codesTable + " (" + "code_id CHAR(36) NOT NULL," - + "device_id_hash CHAR(44) NOT NULL," + "link_code_hash CHAR(44) NOT NULL CONSTRAINT " - + Utils.getConstraintName(schema, codesTable, "link_code_hash", "key") + " UNIQUE," - + "created_at BIGINT NOT NULL," + "CONSTRAINT " - + Utils.getConstraintName(schema, codesTable, null, "pkey") + " PRIMARY KEY (code_id)," + "CONSTRAINT " - + Utils.getConstraintName(schema, codesTable, "device_id_hash", "fkey") - + " FOREIGN KEY (device_id_hash) " + "REFERENCES " - + Config.getConfig(start).getPasswordlessDevicesTable() - + "(device_id_hash) ON DELETE CASCADE ON UPDATE CASCADE);"; + return "CREATE TABLE IF NOT EXISTS " + codesTable + " (" + + "app_id VARCHAR(64) DEFAULT 'public'," + + "tenant_id VARCHAR(64) DEFAULT 'public'," + + "code_id CHAR(36) NOT NULL," + + "device_id_hash CHAR(44) NOT NULL," + + "link_code_hash CHAR(44) NOT NULL," + + "created_at BIGINT NOT NULL," + + "CONSTRAINT " + Utils.getConstraintName(schema, codesTable, "link_code_hash", "key") + + " UNIQUE (app_id, tenant_id, link_code_hash)," + + "CONSTRAINT " + Utils.getConstraintName(schema, codesTable, null, "pkey") + + " PRIMARY KEY (app_id, tenant_id, code_id)," + + "CONSTRAINT " + Utils.getConstraintName(schema, codesTable, "device_id_hash", "fkey") + + " FOREIGN KEY (app_id, tenant_id, device_id_hash)" + + " REFERENCES " + Config.getConfig(start).getPasswordlessDevicesTable() + "(app_id, tenant_id, device_id_hash)" + + " ON DELETE CASCADE ON UPDATE CASCADE" + + ");"; } public static String getQueryToCreateDeviceEmailIndex(Start start) { return "CREATE INDEX passwordless_devices_email_index ON " - + Config.getConfig(start).getPasswordlessDevicesTable() + " (email);"; // USING hash + + Config.getConfig(start).getPasswordlessDevicesTable() + " (app_id, tenant_id, email);"; // USING hash } public static String getQueryToCreateDevicePhoneNumberIndex(Start start) { return "CREATE INDEX passwordless_devices_phone_number_index ON " - + Config.getConfig(start).getPasswordlessDevicesTable() + " (phone_number);"; // USING hash + + Config.getConfig(start).getPasswordlessDevicesTable() + " (app_id, tenant_id, phone_number);"; // USING hash } public static String getQueryToCreateCodeDeviceIdHashIndex(Start start) { return "CREATE INDEX IF NOT EXISTS passwordless_codes_device_id_hash_index ON " - + Config.getConfig(start).getPasswordlessCodesTable() + "(device_id_hash);"; + + Config.getConfig(start).getPasswordlessCodesTable() + "(app_id, tenant_id, device_id_hash);"; } public static String getQueryToCreateCodeCreatedAtIndex(Start start) { return "CREATE INDEX passwordless_codes_created_at_index ON " - + Config.getConfig(start).getPasswordlessCodesTable() + "(created_at);"; + + Config.getConfig(start).getPasswordlessCodesTable() + "(app_id, tenant_id, created_at);"; } - public static void createDeviceWithCode(Start start, String email, String phoneNumber, String linkCodeSalt, - PasswordlessCode code) throws StorageTransactionLogicException, StorageQueryException { + public static void createDeviceWithCode(Start start, TenantIdentifier tenantIdentifier, String email, String phoneNumber, String linkCodeSalt, + PasswordlessCode code) throws StorageTransactionLogicException, StorageQueryException { start.startTransaction(con -> { Connection sqlCon = (Connection) con.getConnection(); try { String QUERY = "INSERT INTO " + getConfig(start).getPasswordlessDevicesTable() - + "(device_id_hash, email, phone_number, link_code_salt, failed_attempts)" - + " VALUES(?, ?, ?, ?, 0)"; + + "(app_id, tenant_id, device_id_hash, email, phone_number, link_code_salt, failed_attempts)" + + " VALUES(?, ?, ?, ?, ?, ?, 0)"; update(sqlCon, QUERY, pst -> { - pst.setString(1, code.deviceIdHash); - pst.setString(2, email); - pst.setString(3, phoneNumber); - pst.setString(4, linkCodeSalt); + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, code.deviceIdHash); + pst.setString(4, email); + pst.setString(5, phoneNumber); + pst.setString(6, linkCodeSalt); }); - createCode_Transaction(start, sqlCon, code); + createCode_Transaction(start, sqlCon, tenantIdentifier, code); sqlCon.commit(); } catch (SQLException throwables) { throw new StorageTransactionLogicException(throwables); @@ -123,12 +173,18 @@ public static void createDeviceWithCode(Start start, String email, String phoneN }, TransactionIsolationLevel.REPEATABLE_READ); } - public static PasswordlessDevice getDevice_Transaction(Start start, Connection con, String deviceIdHash) + public static PasswordlessDevice getDevice_Transaction(Start start, Connection con, + TenantIdentifier tenantIdentifier, String deviceIdHash) throws StorageQueryException, SQLException { String QUERY = "SELECT device_id_hash, email, phone_number, link_code_salt, failed_attempts FROM " - + getConfig(start).getPasswordlessDevicesTable() + " WHERE device_id_hash = ? FOR UPDATE"; - return execute(con, QUERY, pst -> pst.setString(1, deviceIdHash), result -> { + + getConfig(start).getPasswordlessDevicesTable() + + " WHERE app_id = ? AND tenant_id = ? AND device_id_hash = ? FOR UPDATE"; + return execute(con, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, deviceIdHash); + }, result -> { if (result.next()) { return PasswordlessDeviceRowMapper.getInstance().mapOrThrow(result); } @@ -136,55 +192,113 @@ public static PasswordlessDevice getDevice_Transaction(Start start, Connection c }); } - public static void incrementDeviceFailedAttemptCount_Transaction(Start start, Connection con, String deviceIdHash) + public static void incrementDeviceFailedAttemptCount_Transaction(Start start, Connection con, + TenantIdentifier tenantIdentifier, String deviceIdHash) throws SQLException, StorageQueryException { String QUERY = "UPDATE " + getConfig(start).getPasswordlessDevicesTable() - + " SET failed_attempts = failed_attempts + 1 WHERE device_id_hash = ?"; + + " SET failed_attempts = failed_attempts + 1" + + " WHERE app_id = ? AND tenant_id = ? AND device_id_hash = ?"; - update(con, QUERY, pst -> pst.setString(1, deviceIdHash)); + update(con, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, deviceIdHash); + }); } - public static void deleteDevice_Transaction(Start start, Connection con, String deviceIdHash) + public static void deleteDevice_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, String deviceIdHash) throws SQLException, StorageQueryException { - String QUERY = "DELETE FROM " + getConfig(start).getPasswordlessDevicesTable() + " WHERE device_id_hash = ?"; - update(con, QUERY, pst -> pst.setString(1, deviceIdHash)); + String QUERY = "DELETE FROM " + getConfig(start).getPasswordlessDevicesTable() + + " WHERE app_id = ? AND tenant_id = ? AND device_id_hash = ?"; + update(con, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, deviceIdHash); + }); } - public static void deleteDevicesByPhoneNumber_Transaction(Start start, Connection con, @Nonnull String phoneNumber) + public static void deleteDevicesByPhoneNumber_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, @Nonnull String phoneNumber) throws SQLException, StorageQueryException { - String QUERY = "DELETE FROM " + getConfig(start).getPasswordlessDevicesTable() + " WHERE phone_number = ?"; + String QUERY = "DELETE FROM " + getConfig(start).getPasswordlessDevicesTable() + + " WHERE app_id = ? AND tenant_id = ? AND phone_number = ?"; - update(con, QUERY, pst -> pst.setString(1, phoneNumber)); + update(con, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, phoneNumber); + }); } - public static void deleteDevicesByEmail_Transaction(Start start, Connection con, @Nonnull String email) + public static void deleteDevicesByPhoneNumber_Transaction(Start start, Connection con, AppIdentifier appIdentifier, @Nonnull String phoneNumber, String userId) throws SQLException, StorageQueryException { - String QUERY = "DELETE FROM " + getConfig(start).getPasswordlessDevicesTable() + " WHERE email = ?"; + String QUERY = "DELETE FROM " + getConfig(start).getPasswordlessDevicesTable() + + " WHERE app_id = ? AND phone_number = ? AND tenant_id IN (" + + " SELECT tenant_id FROM " + getConfig(start).getPasswordlessUserToTenantTable() + + " WHERE app_id = ? AND user_id = ?" + + ")"; + + update(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, phoneNumber); + pst.setString(3, appIdentifier.getAppId()); + pst.setString(4, userId); + }); + } - update(con, QUERY, pst -> pst.setString(1, email)); + public static void deleteDevicesByEmail_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, @Nonnull String email) + throws SQLException, StorageQueryException { + + String QUERY = "DELETE FROM " + getConfig(start).getPasswordlessDevicesTable() + + " WHERE app_id = ? AND tenant_id = ? AND email = ?"; + + update(con, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, email); + }); + } + + public static void deleteDevicesByEmail_Transaction(Start start, Connection con, AppIdentifier appIdentifier, @Nonnull String email, String userId) + throws SQLException, StorageQueryException { + + String QUERY = "DELETE FROM " + getConfig(start).getPasswordlessDevicesTable() + + " WHERE app_id = ? AND email = ? AND tenant_id IN (" + + " SELECT tenant_id FROM " + getConfig(start).getPasswordlessUserToTenantTable() + + " WHERE app_id = ? AND user_id = ?" + + ")"; + + update(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, email); + pst.setString(3, appIdentifier.getAppId()); + pst.setString(4, userId); + }); } - private static void createCode_Transaction(Start start, Connection con, PasswordlessCode code) + private static void createCode_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, PasswordlessCode code) throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + getConfig(start).getPasswordlessCodesTable() - + "(code_id, device_id_hash, link_code_hash, created_at)" + " VALUES(?, ?, ?, ?)"; + + "(app_id, tenant_id, code_id, device_id_hash, link_code_hash, created_at)" + + " VALUES(?, ?, ?, ?, ?, ?)"; update(con, QUERY, pst -> { - pst.setString(1, code.id); - pst.setString(2, code.deviceIdHash); - pst.setString(3, code.linkCodeHash); - pst.setLong(4, code.createdAt); + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, code.id); + pst.setString(4, code.deviceIdHash); + pst.setString(5, code.linkCodeHash); + pst.setLong(6, code.createdAt); }); } - public static void createCode(Start start, PasswordlessCode code) + public static void createCode(Start start, TenantIdentifier tenantIdentifier, PasswordlessCode code) throws StorageTransactionLogicException, StorageQueryException { start.startTransaction(con -> { Connection sqlCon = (Connection) con.getConnection(); try { - PasswordlessQueries.createCode_Transaction(start, sqlCon, code); + PasswordlessQueries.createCode_Transaction(start, sqlCon, tenantIdentifier, code); sqlCon.commit(); } catch (SQLException e) { throw new StorageTransactionLogicException(e); @@ -193,13 +307,18 @@ public static void createCode(Start start, PasswordlessCode code) }); } - public static PasswordlessCode[] getCodesOfDevice_Transaction(Start start, Connection con, String deviceIdHash) + public static PasswordlessCode[] getCodesOfDevice_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, String deviceIdHash) throws StorageQueryException, SQLException { // We do not lock here, since the device is already locked earlier in the transaction. String QUERY = "SELECT code_id, device_id_hash, link_code_hash, created_at FROM " - + getConfig(start).getPasswordlessCodesTable() + " WHERE device_id_hash = ?"; - - return execute(con, QUERY, pst -> pst.setString(1, deviceIdHash), result -> { + + getConfig(start).getPasswordlessCodesTable() + + " WHERE app_id = ? AND tenant_id = ? AND device_id_hash = ?"; + + return execute(con, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, deviceIdHash); + }, result -> { List temp = new ArrayList<>(); while (result.next()) { temp.add(PasswordlessCodeRowMapper.getInstance().mapOrThrow(result)); @@ -212,13 +331,18 @@ public static PasswordlessCode[] getCodesOfDevice_Transaction(Start start, Conne }); } - public static PasswordlessCode getCodeByLinkCodeHash_Transaction(Start start, Connection con, String linkCodeHash) + public static PasswordlessCode getCodeByLinkCodeHash_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, String linkCodeHash) throws StorageQueryException, SQLException { // We do not lock here, since the device is already locked earlier in the transaction. String QUERY = "SELECT code_id, device_id_hash, link_code_hash, created_at FROM " - + getConfig(start).getPasswordlessCodesTable() + " WHERE link_code_hash = ?"; - - return execute(con, QUERY, pst -> pst.setString(1, linkCodeHash), result -> { + + getConfig(start).getPasswordlessCodesTable() + + " WHERE app_id = ? AND tenant_id = ? AND link_code_hash = ?"; + + return execute(con, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, linkCodeHash); + }, result -> { if (result.next()) { return PasswordlessCodeRowMapper.getInstance().mapOrThrow(result); } @@ -226,36 +350,66 @@ public static PasswordlessCode getCodeByLinkCodeHash_Transaction(Start start, Co }); } - public static void deleteCode_Transaction(Start start, Connection con, String codeId) + public static void deleteCode_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, String codeId) throws SQLException, StorageQueryException { - String QUERY = "DELETE FROM " + getConfig(start).getPasswordlessCodesTable() + " WHERE code_id = ?"; + String QUERY = "DELETE FROM " + getConfig(start).getPasswordlessCodesTable() + + " WHERE app_id = ? AND tenant_id = ? AND code_id = ?"; - update(con, QUERY, pst -> pst.setString(1, codeId)); + update(con, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, codeId); + }); } - public static void createUser(Start start, UserInfo user) + public static void createUser(Start start, TenantIdentifier tenantIdentifier, UserInfo user) throws StorageTransactionLogicException, StorageQueryException { start.startTransaction(con -> { Connection sqlCon = (Connection) con.getConnection(); try { - { + { // app_id_to_user_id + String QUERY = "INSERT INTO " + getConfig(start).getAppIdToUserIdTable() + + "(app_id, user_id)" + " VALUES(?, ?)"; + update(sqlCon, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, user.id); + }); + } + + { // all_auth_recipe_users String QUERY = "INSERT INTO " + getConfig(start).getUsersTable() - + "(user_id, recipe_id, time_joined)" + " VALUES(?, ?, ?)"; + + "(app_id, tenant_id, user_id, recipe_id, time_joined)" + " VALUES(?, ?, ?, ?, ?)"; update(sqlCon, QUERY, pst -> { - pst.setString(1, user.id); - pst.setString(2, PASSWORDLESS.toString()); - pst.setLong(3, user.timeJoined); + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, user.id); + pst.setString(4, PASSWORDLESS.toString()); + pst.setLong(5, user.timeJoined); }); } - { + { // passwordless_users String QUERY = "INSERT INTO " + getConfig(start).getPasswordlessUsersTable() - + "(user_id, email, phone_number, time_joined)" + " VALUES(?, ?, ?, ?)"; + + "(app_id, user_id, email, phone_number, time_joined)" + " VALUES(?, ?, ?, ?, ?)"; + update(sqlCon, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, user.id); + pst.setString(3, user.email); + pst.setString(4, user.phoneNumber); + pst.setLong(5, user.timeJoined); + }); + } + + { // passwordless_user_to_tenant + String QUERY = "INSERT INTO " + getConfig(start).getPasswordlessUserToTenantTable() + + "(app_id, tenant_id, user_id, email, phone_number)" + " VALUES(?, ?, ?, ?, ?)"; + update(sqlCon, QUERY, pst -> { - pst.setString(1, user.id); - pst.setString(2, user.email); - pst.setString(3, user.phoneNumber); - pst.setLong(4, user.timeJoined); + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, user.id); + pst.setString(4, user.email); + pst.setString(5, user.phoneNumber); }); } sqlCon.commit(); @@ -266,41 +420,64 @@ public static void createUser(Start start, UserInfo user) }); } - public static void deleteUser(Start start, String userId) + private static UserInfoWithTenantId[] getUserInfosWithTenant(Start start, Connection con, AppIdentifier appIdentifier, String userId) + throws StorageQueryException, SQLException { + String QUERY = "SELECT pl_users.user_id as user_id, pl_users.email as email, " + + "pl_users.phone_number as phone_number, pl_users_to_tenant.tenant_id as tenant_id " + + "FROM " + getConfig(start).getPasswordlessUsersTable() + " AS pl_users " + + "JOIN " + getConfig(start).getPasswordlessUserToTenantTable() + " AS pl_users_to_tenant " + + "ON pl_users.app_id = pl_users_to_tenant.app_id AND pl_users.user_id = pl_users_to_tenant.user_id " + + "WHERE pl_users_to_tenant.app_id = ? AND pl_users_to_tenant.user_id = ?"; + return execute(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }, result -> { + List userInfos = new ArrayList<>(); + + while (result.next()) { + userInfos.add(new UserInfoWithTenantId( + result.getString("user_id"), + result.getString("tenant_id"), + result.getString("email"), + result.getString("phoneNumber") + )); + PasswordlessDeviceRowMapper.getInstance().mapOrThrow(result); + } + return userInfos.toArray(new UserInfoWithTenantId[0]); + }); + } + + public static void deleteUser(Start start, AppIdentifier appIdentifier, String userId) throws StorageQueryException, StorageTransactionLogicException { start.startTransaction(con -> { Connection sqlCon = (Connection) con.getConnection(); try { + UserInfoWithTenantId[] userInfos = getUserInfosWithTenant(start, sqlCon, appIdentifier, userId); + { - String QUERY = "DELETE FROM " + getConfig(start).getUsersTable() - + " WHERE user_id = ? AND recipe_id = ?"; + String QUERY = "DELETE FROM " + getConfig(start).getAppIdToUserIdTable() + + " WHERE app_id = ? AND user_id = ?"; update(sqlCon, QUERY, pst -> { - pst.setString(1, userId); - pst.setString(2, PASSWORDLESS.toString()); + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); }); } - UserInfo user; - { - String QUERY = "DELETE FROM " + Config.getConfig(start).getPasswordlessUsersTable() - + " WHERE user_id = ? RETURNING user_id, email, phone_number, time_joined"; - - user = execute(sqlCon, QUERY, pst -> pst.setString(1, userId), result -> { - if (result.next()) { - return UserInfoRowMapper.getInstance().mapOrThrow(result); - } - return null; - }); - - } - - if (user != null) { - if (user.email != null) { - deleteDevicesByEmail_Transaction(start, sqlCon, user.email); + for (UserInfoWithTenantId userInfo : userInfos) { + if (userInfo.email != null) { + deleteDevicesByEmail_Transaction(start, sqlCon, + new TenantIdentifier( + appIdentifier.getConnectionUriDomain(), appIdentifier.getAppId(), + userInfo.tenantId, appIdentifier.getStorage()), + userInfo.email); } - if (user.phoneNumber != null) { - deleteDevicesByPhoneNumber_Transaction(start, sqlCon, user.phoneNumber); + if (userInfo.phoneNumber != null) { + deleteDevicesByPhoneNumber_Transaction(start, sqlCon, + new TenantIdentifier( + appIdentifier.getConnectionUriDomain(), appIdentifier.getAppId(), + userInfo.tenantId, appIdentifier.getStorage()), + userInfo.phoneNumber); } } @@ -312,34 +489,65 @@ public static void deleteUser(Start start, String userId) }); } - public static int updateUserEmail_Transaction(Start start, Connection con, String userId, String email) + public static int updateUserEmail_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String userId, String email) throws SQLException, StorageQueryException { - String QUERY = "UPDATE " + Config.getConfig(start).getPasswordlessUsersTable() - + " SET email = ? WHERE user_id = ?"; - - return update(con, QUERY, pst -> { - pst.setString(1, email); - pst.setString(2, userId); - }); + { + String QUERY = "UPDATE " + Config.getConfig(start).getPasswordlessUserToTenantTable() + + " SET email = ? WHERE app_id = ? AND user_id = ?"; + + update(con, QUERY, pst -> { + pst.setString(1, email); + pst.setString(2, appIdentifier.getAppId()); + pst.setString(3, userId); + }); + } + { + String QUERY = "UPDATE " + Config.getConfig(start).getPasswordlessUsersTable() + + " SET email = ? WHERE app_id = ? AND user_id = ?"; + + return update(con, QUERY, pst -> { + pst.setString(1, email); + pst.setString(2, appIdentifier.getAppId()); + pst.setString(3, userId); + }); + } } - public static int updateUserPhoneNumber_Transaction(Start start, Connection con, String userId, String phoneNumber) + public static int updateUserPhoneNumber_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String userId, String phoneNumber) throws SQLException, StorageQueryException { - String QUERY = "UPDATE " + Config.getConfig(start).getPasswordlessUsersTable() - + " SET phone_number = ? WHERE user_id = ?"; - - return update(con, QUERY, pst -> { - pst.setString(1, phoneNumber); - pst.setString(2, userId); - }); + { + String QUERY = "UPDATE " + Config.getConfig(start).getPasswordlessUserToTenantTable() + + " SET phone_number = ? WHERE app_id = ? AND user_id = ?"; + + update(con, QUERY, pst -> { + pst.setString(1, phoneNumber); + pst.setString(2, appIdentifier.getAppId()); + pst.setString(3, userId); + }); + } + { + String QUERY = "UPDATE " + Config.getConfig(start).getPasswordlessUsersTable() + + " SET phone_number = ? WHERE app_id = ? AND user_id = ?"; + + return update(con, QUERY, pst -> { + pst.setString(1, phoneNumber); + pst.setString(2, appIdentifier.getAppId()); + pst.setString(3, userId); + }); + } } - public static PasswordlessDevice getDevice(Start start, String deviceIdHash) + public static PasswordlessDevice getDevice(Start start, TenantIdentifier tenantIdentifier, String deviceIdHash) throws StorageQueryException, SQLException { try (Connection con = ConnectionPool.getConnection(start)) { String QUERY = "SELECT device_id_hash, email, phone_number, link_code_salt, failed_attempts FROM " - + getConfig(start).getPasswordlessDevicesTable() + " WHERE device_id_hash = ?"; - return execute(con, QUERY, pst -> pst.setString(1, deviceIdHash), result -> { + + getConfig(start).getPasswordlessDevicesTable() + + " WHERE app_id = ? AND tenant_id = ? AND device_id_hash = ?"; + return execute(con, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, deviceIdHash); + }, result -> { if (result.next()) { return PasswordlessDeviceRowMapper.getInstance().mapOrThrow(result); } @@ -348,12 +556,17 @@ public static PasswordlessDevice getDevice(Start start, String deviceIdHash) } } - public static PasswordlessDevice[] getDevicesByEmail(Start start, @Nonnull String email) + public static PasswordlessDevice[] getDevicesByEmail(Start start, TenantIdentifier tenantIdentifier, @Nonnull String email) throws StorageQueryException, SQLException { String QUERY = "SELECT device_id_hash, email, phone_number, link_code_salt, failed_attempts FROM " - + getConfig(start).getPasswordlessDevicesTable() + " WHERE email = ?"; - - return execute(start, QUERY, pst -> pst.setString(1, email), result -> { + + getConfig(start).getPasswordlessDevicesTable() + + " WHERE app_id = ? AND tenant_id = ? AND email = ?"; + + return execute(start, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, email); + }, result -> { List temp = new ArrayList<>(); while (result.next()) { temp.add(PasswordlessDeviceRowMapper.getInstance().mapOrThrow(result)); @@ -366,12 +579,18 @@ public static PasswordlessDevice[] getDevicesByEmail(Start start, @Nonnull Strin }); } - public static PasswordlessDevice[] getDevicesByPhoneNumber(Start start, @Nonnull String phoneNumber) + public static PasswordlessDevice[] getDevicesByPhoneNumber(Start start, TenantIdentifier tenantIdentifier, + @Nonnull String phoneNumber) throws StorageQueryException, SQLException { String QUERY = "SELECT device_id_hash, email, phone_number, link_code_salt, failed_attempts FROM " - + getConfig(start).getPasswordlessDevicesTable() + " WHERE phone_number = ?"; - - return execute(start, QUERY, pst -> pst.setString(1, phoneNumber), result -> { + + getConfig(start).getPasswordlessDevicesTable() + + " WHERE app_id = ? AND tenant_id = ? AND phone_number = ?"; + + return execute(start, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, phoneNumber); + }, result -> { List temp = new ArrayList<>(); while (result.next()) { temp.add(PasswordlessDeviceRowMapper.getInstance().mapOrThrow(result)); @@ -384,19 +603,24 @@ public static PasswordlessDevice[] getDevicesByPhoneNumber(Start start, @Nonnull }); } - public static PasswordlessCode[] getCodesOfDevice(Start start, String deviceIdHash) + public static PasswordlessCode[] getCodesOfDevice(Start start, TenantIdentifier tenantIdentifier, String deviceIdHash) throws StorageQueryException, SQLException { try (Connection con = ConnectionPool.getConnection(start)) { // We can call the transaction version here because it doesn't lock anything. - return PasswordlessQueries.getCodesOfDevice_Transaction(start, con, deviceIdHash); + return PasswordlessQueries.getCodesOfDevice_Transaction(start, con, tenantIdentifier, deviceIdHash); } } - public static PasswordlessCode[] getCodesBefore(Start start, long time) throws StorageQueryException, SQLException { + public static PasswordlessCode[] getCodesBefore(Start start, TenantIdentifier tenantIdentifier, long time) throws StorageQueryException, SQLException { String QUERY = "SELECT code_id, device_id_hash, link_code_hash, created_at FROM " - + getConfig(start).getPasswordlessCodesTable() + " WHERE created_at < ?"; - - return execute(start, QUERY, pst -> pst.setLong(1, time), result -> { + + getConfig(start).getPasswordlessCodesTable() + + " WHERE app_id = ? AND tenant_id = ? AND created_at < ?"; + + return execute(start, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setLong(3, time); + }, result -> { List temp = new ArrayList<>(); while (result.next()) { temp.add(PasswordlessCodeRowMapper.getInstance().mapOrThrow(result)); @@ -409,11 +633,16 @@ public static PasswordlessCode[] getCodesBefore(Start start, long time) throws S }); } - public static PasswordlessCode getCode(Start start, String codeId) throws StorageQueryException, SQLException { + public static PasswordlessCode getCode(Start start, TenantIdentifier tenantIdentifier, String codeId) throws StorageQueryException, SQLException { String QUERY = "SELECT code_id, device_id_hash, link_code_hash, created_at FROM " - + getConfig(start).getPasswordlessCodesTable() + " WHERE code_id = ?"; - - return execute(start, QUERY, pst -> pst.setString(1, codeId), result -> { + + getConfig(start).getPasswordlessCodesTable() + + " WHERE app_id = ? AND tenant_id = ? AND code_id = ?"; + + return execute(start, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, codeId); + }, result -> { if (result.next()) { return PasswordlessCodeRowMapper.getInstance().mapOrThrow(result); } @@ -421,22 +650,22 @@ public static PasswordlessCode getCode(Start start, String codeId) throws Storag }); } - public static PasswordlessCode getCodeByLinkCodeHash(Start start, String linkCodeHash) + public static PasswordlessCode getCodeByLinkCodeHash(Start start, TenantIdentifier tenantIdentifier, String linkCodeHash) throws StorageQueryException, SQLException { try (Connection con = ConnectionPool.getConnection(start)) { // We can call the transaction version here because it doesn't lock anything. - return PasswordlessQueries.getCodeByLinkCodeHash_Transaction(start, con, linkCodeHash); + return PasswordlessQueries.getCodeByLinkCodeHash_Transaction(start, con, tenantIdentifier, linkCodeHash); } } public static List getUsersByIdList(Start start, List ids) throws SQLException, StorageQueryException { if (ids.size() > 0) { - StringBuilder QUERY = new StringBuilder("SELECT user_id, email, phone_number, time_joined FROM " - + getConfig(start).getPasswordlessUsersTable()); + // No need to filter based on tenantId because the id list is already filtered for a tenant + StringBuilder QUERY = new StringBuilder("SELECT user_id, email, phone_number, time_joined " + + "FROM " + getConfig(start).getPasswordlessUsersTable()); QUERY.append(" WHERE user_id IN ("); for (int i = 0; i < ids.size(); i++) { - QUERY.append("?"); if (i != ids.size() - 1) { // not the last element @@ -461,22 +690,14 @@ public static List getUsersByIdList(Start start, List ids) return Collections.emptyList(); } - public static UserInfo getUserById(Start start, String userId) throws StorageQueryException, SQLException { - List input = new ArrayList<>(); - input.add(userId); - List result = getUsersByIdList(start, input); - if (result.size() == 1) { - return result.get(0); - } - return null; - } - - public static UserInfo getUserByEmail(Start start, @Nonnull String email) - throws StorageQueryException, SQLException { + public static UserInfo getUserById(Start start, AppIdentifier appIdentifier, String userId) throws StorageQueryException, SQLException { String QUERY = "SELECT user_id, email, phone_number, time_joined FROM " - + getConfig(start).getPasswordlessUsersTable() + " WHERE email = ?"; + + getConfig(start).getPasswordlessUsersTable() + " WHERE app_id = ? AND user_id = ?"; - return execute(start, QUERY, pst -> pst.setString(1, email), result -> { + return execute(start, QUERY.toString(), pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }, result -> { if (result.next()) { return UserInfoRowMapper.getInstance().mapOrThrow(result); } @@ -484,12 +705,41 @@ public static UserInfo getUserByEmail(Start start, @Nonnull String email) }); } - public static UserInfo getUserByPhoneNumber(Start start, @Nonnull String phoneNumber) + public static UserInfo getUserByEmail(Start start, TenantIdentifier tenantIdentifier, @Nonnull String email) throws StorageQueryException, SQLException { - String QUERY = "SELECT user_id, email, phone_number, time_joined FROM " - + getConfig(start).getPasswordlessUsersTable() + " WHERE phone_number = ?"; + String QUERY = "SELECT pl_users.user_id as user_id, pl_users.email as email, " + + "pl_users.phone_number as phone_number, pl_users.time_joined as time_joined " + + "FROM " + getConfig(start).getPasswordlessUserToTenantTable() + " AS pl_users_to_tenant " + + "JOIN " + getConfig(start).getPasswordlessUsersTable() + " AS pl_users " + + "ON pl_users.app_id = pl_users_to_tenant.app_id AND pl_users.user_id = pl_users_to_tenant.user_id " + + "WHERE pl_users_to_tenant.app_id = ? AND pl_users_to_tenant.tenant_id = ? AND pl_users_to_tenant.email = ? "; + + return execute(start, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, email); + }, result -> { + if (result.next()) { + return UserInfoRowMapper.getInstance().mapOrThrow(result); + } + return null; + }); + } - return execute(start, QUERY, pst -> pst.setString(1, phoneNumber), result -> { + public static UserInfo getUserByPhoneNumber(Start start, TenantIdentifier tenantIdentifier, @Nonnull String phoneNumber) + throws StorageQueryException, SQLException { + String QUERY = "SELECT pl_users.user_id as user_id, pl_users.email as email, " + + "pl_users.phone_number as phone_number, pl_users.time_joined as time_joined " + + "FROM " + getConfig(start).getPasswordlessUserToTenantTable() + " AS pl_users_to_tenant " + + "JOIN " + getConfig(start).getPasswordlessUsersTable() + " AS pl_users " + + "ON pl_users.app_id = pl_users_to_tenant.app_id AND pl_users.user_id = pl_users_to_tenant.user_id " + + "WHERE pl_users_to_tenant.app_id = ? AND pl_users_to_tenant.tenant_id = ? AND pl_users_to_tenant.phone_number = ? "; + + return execute(start, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, phoneNumber); + }, result -> { if (result.next()) { return UserInfoRowMapper.getInstance().mapOrThrow(result); } @@ -548,4 +798,18 @@ public UserInfo map(ResultSet result) throws Exception { result.getString("phone_number"), result.getLong("time_joined")); } } + + private static class UserInfoWithTenantId { + public final String userId; + public final String tenantId; + public final String email; + public final String phoneNumber; + + public UserInfoWithTenantId(String userId, String tenantId, String email, String phoneNumber) { + this.userId = userId; + this.tenantId = tenantId; + this.email = email; + this.phoneNumber = phoneNumber; + } + } } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/ThirdPartyQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/ThirdPartyQueries.java index e6088af4..cb73b846 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/ThirdPartyQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/ThirdPartyQueries.java @@ -19,7 +19,10 @@ import io.supertokens.pluginInterface.RowMapper; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.thirdparty.UserInfo; +import io.supertokens.storage.postgresql.ConnectionPool; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.config.Config; import io.supertokens.storage.postgresql.utils.Utils; @@ -44,42 +47,97 @@ static String getQueryToCreateUsersTable(Start start) { String thirdPartyUsersTable = Config.getConfig(start).getThirdPartyUsersTable(); // @formatter:off return "CREATE TABLE IF NOT EXISTS " + thirdPartyUsersTable + " (" + + "app_id VARCHAR(64) DEFAULT 'public'," + "third_party_id VARCHAR(28) NOT NULL," + "third_party_user_id VARCHAR(256) NOT NULL," - + "user_id CHAR(36) NOT NULL CONSTRAINT " + - Utils.getConstraintName(schema, thirdPartyUsersTable, "user_id", "key") + " UNIQUE," + + "user_id CHAR(36) NOT NULL," + "email VARCHAR(256) NOT NULL," + "time_joined BIGINT NOT NULL," - + "CONSTRAINT " + Utils.getConstraintName(schema, thirdPartyUsersTable, null, "pkey") + - " PRIMARY KEY (third_party_id, third_party_user_id));"; + + "CONSTRAINT " + Utils.getConstraintName(schema, thirdPartyUsersTable, "app_id", "fkey") + + " FOREIGN KEY(app_id, user_id)" + + " REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable() + " (app_id, user_id) ON DELETE CASCADE," + + "CONSTRAINT " + Utils.getConstraintName(schema, thirdPartyUsersTable, null, "pkey") + + " PRIMARY KEY (app_id, user_id)" + + ");"; // @formatter:on } - public static void signUp(Start start, io.supertokens.pluginInterface.thirdparty.UserInfo userInfo) + public static String getQueryToThirdPartyUserEmailIndex(Start start) { + return "CREATE INDEX IF NOT EXISTS thirdparty_users_email_index ON " + + Config.getConfig(start).getThirdPartyUsersTable() + " (app_id, email);"; + } + + static String getQueryToCreateThirdPartyUserToTenantTable(Start start) { + String schema = Config.getConfig(start).getTableSchema(); + String thirdPartyUserToTenantTable = Config.getConfig(start).getThirdPartyUserToTenantTable(); + // @formatter:off + return "CREATE TABLE IF NOT EXISTS " + thirdPartyUserToTenantTable + " (" + + "app_id VARCHAR(64) DEFAULT 'public'," + + "tenant_id VARCHAR(64) DEFAULT 'public'," + + "user_id CHAR(36) NOT NULL," + + "third_party_id VARCHAR(28) NOT NULL," + + "third_party_user_id VARCHAR(256) NOT NULL," + + "CONSTRAINT " + Utils.getConstraintName(schema, thirdPartyUserToTenantTable, "third_party_user_id", "key") + + " UNIQUE (app_id, tenant_id, third_party_id, third_party_user_id)," + + "CONSTRAINT " + Utils.getConstraintName(schema, thirdPartyUserToTenantTable, null, "pkey") + + " PRIMARY KEY (app_id, tenant_id, user_id)," + + "CONSTRAINT " + Utils.getConstraintName(schema, thirdPartyUserToTenantTable, "user_id", "fkey") + + " FOREIGN KEY (app_id, tenant_id, user_id)" + + " REFERENCES " + Config.getConfig(start).getUsersTable() + "(app_id, tenant_id, user_id) ON DELETE CASCADE" + + ");"; + // @formatter:on + } + + public static void signUp(Start start, TenantIdentifier tenantIdentifier, UserInfo userInfo) throws StorageQueryException, StorageTransactionLogicException { start.startTransaction(con -> { Connection sqlCon = (Connection) con.getConnection(); try { - { + { // app_id_to_user_id + String QUERY = "INSERT INTO " + getConfig(start).getAppIdToUserIdTable() + + "(app_id, user_id)" + " VALUES(?, ?)"; + update(sqlCon, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, userInfo.id); + }); + } + + { // all_auth_recipe_users String QUERY = "INSERT INTO " + getConfig(start).getUsersTable() - + "(user_id, recipe_id, time_joined)" + " VALUES(?, ?, ?)"; + + "(app_id, tenant_id, user_id, recipe_id, time_joined)" + " VALUES(?, ?, ?, ?, ?)"; update(sqlCon, QUERY, pst -> { - pst.setString(1, userInfo.id); - pst.setString(2, THIRD_PARTY.toString()); - pst.setLong(3, userInfo.timeJoined); + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, userInfo.id); + pst.setString(4, THIRD_PARTY.toString()); + pst.setLong(5, userInfo.timeJoined); }); } - { + { // thirdparty_users String QUERY = "INSERT INTO " + getConfig(start).getThirdPartyUsersTable() - + "(third_party_id, third_party_user_id, user_id, email, time_joined)" + + "(app_id, third_party_id, third_party_user_id, user_id, email, time_joined)" + + " VALUES(?, ?, ?, ?, ?, ?)"; + update(sqlCon, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, userInfo.thirdParty.id); + pst.setString(3, userInfo.thirdParty.userId); + pst.setString(4, userInfo.id); + pst.setString(5, userInfo.email); + pst.setLong(6, userInfo.timeJoined); + }); + } + + { // thirdparty_user_to_tenant + String QUERY = "INSERT INTO " + getConfig(start).getThirdPartyUserToTenantTable() + + "(app_id, tenant_id, user_id, third_party_id, third_party_user_id)" + " VALUES(?, ?, ?, ?, ?)"; update(sqlCon, QUERY, pst -> { - pst.setString(1, userInfo.thirdParty.id); - pst.setString(2, userInfo.thirdParty.userId); + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); pst.setString(3, userInfo.id); - pst.setString(4, userInfo.email); - pst.setLong(5, userInfo.timeJoined); + pst.setString(4, userInfo.thirdParty.id); + pst.setString(5, userInfo.thirdParty.userId); }); } @@ -91,25 +149,21 @@ public static void signUp(Start start, io.supertokens.pluginInterface.thirdparty }); } - public static void deleteUser(Start start, String userId) + public static void deleteUser(Start start, AppIdentifier appIdentifier, String userId) throws StorageQueryException, StorageTransactionLogicException { start.startTransaction(con -> { Connection sqlCon = (Connection) con.getConnection(); try { { - String QUERY = "DELETE FROM " + getConfig(start).getUsersTable() - + " WHERE user_id = ? AND recipe_id = ?"; + String QUERY = "DELETE FROM " + getConfig(start).getAppIdToUserIdTable() + + " WHERE app_id = ? AND user_id = ?"; + update(sqlCon, QUERY, pst -> { - pst.setString(1, userId); - pst.setString(2, THIRD_PARTY.toString()); + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); }); } - { - String QUERY = "DELETE FROM " + getConfig(start).getThirdPartyUsersTable() + " WHERE user_id = ? "; - update(sqlCon, QUERY, pst -> pst.setString(1, userId)); - } - sqlCon.commit(); } catch (SQLException throwables) { throw new StorageTransactionLogicException(throwables); @@ -118,23 +172,29 @@ public static void deleteUser(Start start, String userId) }); } - public static UserInfo getThirdPartyUserInfoUsingId(Start start, String userId) + public static UserInfo getThirdPartyUserInfoUsingId(Start start, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { - List input = new ArrayList<>(); - input.add(userId); - List result = getUsersInfoUsingIdList(start, input); - if (result.size() == 1) { - return result.get(0); - } - return null; + String QUERY = "SELECT user_id, third_party_id, third_party_user_id, email, time_joined FROM " + + getConfig(start).getThirdPartyUsersTable() + " WHERE app_id = ? AND user_id = ?"; + + return execute(start, QUERY.toString(), pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }, result -> { + if (result.next()) { + return UserInfoRowMapper.getInstance().mapOrThrow(result); + } + return null; + }); } public static List getUsersInfoUsingIdList(Start start, List ids) throws SQLException, StorageQueryException { if (ids.size() > 0) { + // No need to filter based on tenantId because the id list is already filtered for a tenant StringBuilder QUERY = new StringBuilder( - "SELECT user_id, third_party_id, third_party_user_id, email, time_joined FROM " - + getConfig(start).getThirdPartyUsersTable()); + "SELECT user_id, third_party_id, third_party_user_id, email, time_joined " + + "FROM " + getConfig(start).getThirdPartyUsersTable()); QUERY.append(" WHERE user_id IN ("); for (int i = 0; i < ids.size(); i++) { @@ -162,14 +222,24 @@ public static List getUsersInfoUsingIdList(Start start, List i return Collections.emptyList(); } - public static UserInfo getThirdPartyUserInfoUsingId(Start start, String thirdPartyId, String thirdPartyUserId) + public static UserInfo getThirdPartyUserInfoUsingId(Start start, TenantIdentifier tenantIdentifier, + String thirdPartyId, String thirdPartyUserId) throws SQLException, StorageQueryException { - String QUERY = "SELECT user_id, third_party_id, third_party_user_id, email, time_joined FROM " - + getConfig(start).getThirdPartyUsersTable() + " WHERE third_party_id = ? AND third_party_user_id = ?"; + String QUERY = "SELECT tp_users.user_id as user_id, tp_users.third_party_id as third_party_id, " + + "tp_users.third_party_user_id as third_party_user_id, tp_users.email as email, " + + "tp_users.time_joined as time_joined " + + "FROM " + getConfig(start).getThirdPartyUserToTenantTable() + " AS tp_users_to_tenant " + + "JOIN " + getConfig(start).getThirdPartyUsersTable() + " AS tp_users " + + "ON tp_users.app_id = tp_users_to_tenant.app_id AND tp_users.user_id = tp_users_to_tenant.user_id " + + "WHERE tp_users_to_tenant.app_id = ? AND tp_users_to_tenant.tenant_id = ? " + + "AND tp_users_to_tenant.third_party_id = ? AND tp_users_to_tenant.third_party_user_id = ?"; + return execute(start, QUERY, pst -> { - pst.setString(1, thirdPartyId); - pst.setString(2, thirdPartyUserId); + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, thirdPartyId); + pst.setString(4, thirdPartyUserId); }, result -> { if (result.next()) { return UserInfoRowMapper.getInstance().mapOrThrow(result); @@ -178,29 +248,42 @@ public static UserInfo getThirdPartyUserInfoUsingId(Start start, String thirdPar }); } - public static void updateUserEmail_Transaction(Start start, Connection con, String thirdPartyId, - String thirdPartyUserId, String newEmail) + public static void updateUserEmail_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, + String thirdPartyId, String thirdPartyUserId, String newEmail) throws SQLException, StorageQueryException { String QUERY = "UPDATE " + getConfig(start).getThirdPartyUsersTable() - + " SET email = ? WHERE third_party_id = ? AND third_party_user_id = ?"; + + " SET email = ? WHERE app_id = ? AND user_id IN (" + + " SELECT user_id FROM " + getConfig(start).getThirdPartyUserToTenantTable() + + " WHERE app_id = ? AND tenant_id = ? AND third_party_id = ? AND third_party_user_id = ?" + + ")"; update(con, QUERY, pst -> { pst.setString(1, newEmail); - pst.setString(2, thirdPartyId); - pst.setString(3, thirdPartyUserId); + pst.setString(2, tenantIdentifier.getAppId()); + pst.setString(3, tenantIdentifier.getAppId()); + pst.setString(4, tenantIdentifier.getTenantId()); + pst.setString(5, thirdPartyId); + pst.setString(6, thirdPartyUserId); }); } - public static UserInfo getUserInfoUsingId_Transaction(Start start, Connection con, String thirdPartyId, + public static UserInfo getUserInfoUsingId_Transaction(Start start, Connection con, + TenantIdentifier tenantIdentifier, String thirdPartyId, String thirdPartyUserId) throws SQLException, StorageQueryException { String QUERY = "SELECT user_id, third_party_id, third_party_user_id, email, time_joined FROM " + getConfig(start).getThirdPartyUsersTable() - + " WHERE third_party_id = ? AND third_party_user_id = ? FOR UPDATE"; + + " WHERE app_id = ? AND user_id IN (" + + " SELECT user_id FROM " + getConfig(start).getThirdPartyUserToTenantTable() + + " WHERE app_id = ? AND tenant_id = ? AND third_party_id = ? AND third_party_user_id = ?" + + ") FOR UPDATE"; return execute(con, QUERY, pst -> { - pst.setString(1, thirdPartyId); - pst.setString(2, thirdPartyUserId); + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getAppId()); + pst.setString(3, tenantIdentifier.getTenantId()); + pst.setString(4, thirdPartyId); + pst.setString(5, thirdPartyUserId); }, result -> { if (result.next()) { return UserInfoRowMapper.getInstance().mapOrThrow(result); @@ -209,19 +292,29 @@ public static UserInfo getUserInfoUsingId_Transaction(Start start, Connection co }); } - public static UserInfo[] getThirdPartyUsersByEmail(Start start, @NotNull String email) + public static UserInfo[] getThirdPartyUsersByEmail(Start start, TenantIdentifier tenantIdentifier, + @NotNull String email) throws SQLException, StorageQueryException { - String QUERY = "SELECT user_id, third_party_id, third_party_user_id, email, time_joined FROM " - + Config.getConfig(start).getThirdPartyUsersTable() + " WHERE email = ?"; - return execute(start, QUERY, pst -> pst.setString(1, email), result -> { - List users = new ArrayList<>(); + String QUERY = "SELECT tp_users.user_id as user_id, tp_users.third_party_id as third_party_id, " + + "tp_users.third_party_user_id as third_party_user_id, tp_users.email as email, " + + "tp_users.time_joined as time_joined " + + "FROM " + getConfig(start).getThirdPartyUsersTable() + " AS tp_users " + + "JOIN " + getConfig(start).getThirdPartyUserToTenantTable() + " AS tp_users_to_tenant " + + "ON tp_users.app_id = tp_users_to_tenant.app_id AND tp_users.user_id = tp_users_to_tenant.user_id " + + "WHERE tp_users_to_tenant.app_id = ? AND tp_users_to_tenant.tenant_id = ? AND tp_users.email = ? " + + "ORDER BY time_joined"; + return execute(start, QUERY.toString(), pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, email); + }, result -> { + List finalResult = new ArrayList<>(); while (result.next()) { - users.add(UserInfoRowMapper.getInstance().mapOrThrow(result)); + finalResult.add(UserInfoRowMapper.getInstance().mapOrThrow(result)); } - - return users.toArray(UserInfo[]::new); + return finalResult.toArray(new UserInfo[0]); }); } diff --git a/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java b/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java index fcdae040..a71d77c2 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java @@ -34,6 +34,7 @@ import io.supertokens.pluginInterface.jwt.sqlstorage.JWTRecipeSQLStorage; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.thirdparty.exception.DuplicateThirdPartyUserException; import io.supertokens.storageLayer.StorageLayer; import org.junit.AfterClass; @@ -156,7 +157,8 @@ public void updateUsersEmail_TransactionExceptions() throws InterruptedException, StorageQueryException, NoSuchAlgorithmException, InvalidKeyException, SignatureException, InvalidAlgorithmParameterException, NoSuchPaddingException, BadPaddingException, UnsupportedEncodingException, InvalidKeySpecException, IllegalBlockSizeException, - StorageTransactionLogicException, DuplicateUserIdException, DuplicateEmailException { + StorageTransactionLogicException, DuplicateUserIdException, DuplicateEmailException, + TenantOrAppNotFoundException { { String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); @@ -219,9 +221,14 @@ public void updateIsEmailVerified_TransactionExceptions() String userEmail = "useremail@asdf.fdas"; storage.startTransaction(conn -> { - storage.updateIsEmailVerified_Transaction(new AppIdentifier(null, null), conn, userId, userEmail, true); + try { + storage.updateIsEmailVerified_Transaction(new AppIdentifier(null, null), conn, userId, userEmail, + true); + storage.updateIsEmailVerified_Transaction(new AppIdentifier(null, null), conn, userId, userEmail, true); + } catch (TenantOrAppNotFoundException e) { + throw new RuntimeException(e); + } // The insert in this call throws, but it's swallowed in the method - storage.updateIsEmailVerified_Transaction(new AppIdentifier(null, null), conn, userId, userEmail, true); return true; }); @@ -233,13 +240,19 @@ public void updateIsEmailVerified_TransactionExceptions() throw new StorageTransactionLogicException(new Exception("This should throw")); } catch (StorageQueryException ex) { // expected + } catch (TenantOrAppNotFoundException e) { + throw new RuntimeException(e); } return true; }); storage.startTransaction(conn -> { - storage.updateIsEmailVerified_Transaction(new AppIdentifier(null, null), conn, userId, userEmail, - false); + try { + storage.updateIsEmailVerified_Transaction(new AppIdentifier(null, null), conn, userId, userEmail, + false); + } catch (TenantOrAppNotFoundException e) { + throw new RuntimeException(e); + } return true; }); From 4c36155ad867993d3042d71d70b70fc6aaa35935 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Sat, 25 Mar 2023 11:25:53 +0530 Subject: [PATCH 043/106] fix: to support PR comments on core (#65) * fix: from core pr comments * fix: updated tenant identifier conversion --- .../supertokens/storage/postgresql/Start.java | 18 ++++++++++++++---- .../queries/PasswordlessQueries.java | 6 +++--- .../queries/UserIdMappingQueries.java | 9 ++++----- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 71abb603..e8099033 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -1160,6 +1160,17 @@ public long getUsersCount(TenantIdentifier tenantIdentifier, RECIPE_ID[] include } } + @Override + public long getUsersCount(AppIdentifier appIdentifier, RECIPE_ID[] includeRecipeIds) + throws StorageQueryException { + // TODO.. + try { + return GeneralQueries.getUsersCount(this, includeRecipeIds); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + @Override public AuthRecipeUserInfo[] getUsers(TenantIdentifier tenantIdentifier, @NotNull Integer limit, @NotNull String timeJoinedOrder, @@ -1986,12 +1997,11 @@ public boolean updateOrDeleteExternalUserIdInfo(AppIdentifier appIdentifier, Str } @Override - public HashMap getUserIdMappingForSuperTokensIds(AppIdentifier appIdentifier, - ArrayList userIds) + public HashMap getUserIdMappingForSuperTokensIds(ArrayList userIds) throws StorageQueryException { - // TODO.. try { - return UserIdMappingQueries.getUserIdMappingWithUserIds(this, appIdentifier, userIds); + + return UserIdMappingQueries.getUserIdMappingWithUserIds(this, userIds); } catch (SQLException e) { throw new StorageQueryException(e); } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java index f842271c..cb06cd80 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java @@ -469,14 +469,14 @@ public static void deleteUser(Start start, AppIdentifier appIdentifier, String u deleteDevicesByEmail_Transaction(start, sqlCon, new TenantIdentifier( appIdentifier.getConnectionUriDomain(), appIdentifier.getAppId(), - userInfo.tenantId, appIdentifier.getStorage()), + userInfo.tenantId), userInfo.email); } if (userInfo.phoneNumber != null) { deleteDevicesByPhoneNumber_Transaction(start, sqlCon, new TenantIdentifier( appIdentifier.getConnectionUriDomain(), appIdentifier.getAppId(), - userInfo.tenantId, appIdentifier.getStorage()), + userInfo.tenantId), userInfo.phoneNumber); } } @@ -694,7 +694,7 @@ public static UserInfo getUserById(Start start, AppIdentifier appIdentifier, Str String QUERY = "SELECT user_id, email, phone_number, time_joined FROM " + getConfig(start).getPasswordlessUsersTable() + " WHERE app_id = ? AND user_id = ?"; - return execute(start, QUERY.toString(), pst -> { + return execute(start, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); pst.setString(2, userId); }, result -> { diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/UserIdMappingQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/UserIdMappingQueries.java index dd577cf2..5a16b8cb 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/UserIdMappingQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/UserIdMappingQueries.java @@ -120,16 +120,16 @@ public static UserIdMapping[] getUserIdMappingWithEitherSuperTokensUserIdOrExter } - public static HashMap getUserIdMappingWithUserIds(Start start, AppIdentifier appIdentifier, - ArrayList userIds) + public static HashMap getUserIdMappingWithUserIds(Start start, ArrayList userIds) throws SQLException, StorageQueryException { if (userIds.size() == 0) { return new HashMap<>(); } + // No need to filter based on tenantId because the id list is already filtered for a tenant StringBuilder QUERY = new StringBuilder( - "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable() + " WHERE app_id = ? AND supertokens_user_id IN ("); + "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable() + " WHERE supertokens_user_id IN ("); for (int i = 0; i < userIds.size(); i++) { QUERY.append("?"); if (i != userIds.size() - 1) { @@ -139,10 +139,9 @@ public static HashMap getUserIdMappingWithUserIds(Start start, A } QUERY.append(")"); return execute(start, QUERY.toString(), pst -> { - pst.setString(1, appIdentifier.getAppId()); for (int i = 0; i < userIds.size(); i++) { // i+1 cause this starts with 1 and not 0 - pst.setString(i + 2, userIds.get(i)); + pst.setString(i + 1, userIds.get(i)); } }, result -> { HashMap userIdMappings = new HashMap<>(); From 0c467d12691bfe904f8d697ecbe64ba00117d6b6 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Tue, 28 Mar 2023 17:54:19 +0530 Subject: [PATCH 044/106] fix: Multitenant userroles (#69) * fix: user roles impl * fix: handling fkey * fix: transaction fix * fix: transaction fix --- .../supertokens/storage/postgresql/Start.java | 82 ++++--- .../postgresql/queries/UserRolesQueries.java | 224 +++++++++++++----- 2 files changed, 199 insertions(+), 107 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index e8099033..e122eb79 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -20,10 +20,7 @@ import ch.qos.logback.classic.Logger; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; -import io.supertokens.pluginInterface.KeyValueInfo; -import io.supertokens.pluginInterface.LOG_LEVEL; -import io.supertokens.pluginInterface.RECIPE_ID; -import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.*; import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.dashboard.DashboardSessionInfo; import io.supertokens.pluginInterface.dashboard.DashboardUser; @@ -677,7 +674,11 @@ public void addInfoToNonAuthRecipesBasedOnUserId(String className, String userId try { String role = "testRole"; this.startTransaction(con -> { - createNewRoleOrDoNothingIfExists_Transaction(new TenantIdentifier(null, null, null), con, role); + try { + createNewRoleOrDoNothingIfExists_Transaction(new AppIdentifier(null, null), con, role); + } catch (TenantOrAppNotFoundException e) { + throw new IllegalStateException(e); + } return null; }); try { @@ -1694,10 +1695,10 @@ public int deleteUserMetadata(AppIdentifier appIdentifier, String userId) throws @Override public void addRoleToUser(TenantIdentifier tenantIdentifier, String userId, String role) - throws StorageQueryException, UnknownRoleException, DuplicateUserRoleMappingException { - // TODO... + throws StorageQueryException, UnknownRoleException, DuplicateUserRoleMappingException, + TenantOrAppNotFoundException { try { - UserRolesQueries.addRoleToUser(this, userId, role); + UserRolesQueries.addRoleToUser(this, tenantIdentifier, userId, role); } catch (SQLException e) { if (e instanceof PSQLException) { PostgreSQLConfig config = Config.getConfig(this); @@ -1708,6 +1709,9 @@ public void addRoleToUser(TenantIdentifier tenantIdentifier, String userId, Stri if (isPrimaryKeyError(serverErrorMessage, config.getUserRolesTable())) { throw new DuplicateUserRoleMappingException(); } + if (isForeignKeyConstraintError(serverErrorMessage, config.getUserRolesTable(), "tenant_id")) { + throw new TenantOrAppNotFoundException(tenantIdentifier); + } } throw new StorageQueryException(e); } @@ -1717,8 +1721,7 @@ public void addRoleToUser(TenantIdentifier tenantIdentifier, String userId, Stri @Override public String[] getRolesForUser(TenantIdentifier tenantIdentifier, String userId) throws StorageQueryException { try { - // TODO.. - return UserRolesQueries.getRolesForUser(this, userId); + return UserRolesQueries.getRolesForUser(this, tenantIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1726,8 +1729,7 @@ public String[] getRolesForUser(TenantIdentifier tenantIdentifier, String userId private String[] getRolesForUser(AppIdentifier appIdentifier, String userId) throws StorageQueryException { try { - // TODO.. - return UserRolesQueries.getRolesForUser(this, userId); + return UserRolesQueries.getRolesForUser(this, appIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1736,8 +1738,7 @@ private String[] getRolesForUser(AppIdentifier appIdentifier, String userId) thr @Override public String[] getUsersForRole(TenantIdentifier tenantIdentifier, String role) throws StorageQueryException { try { - // TODO.. - return UserRolesQueries.getUsersForRole(this, role); + return UserRolesQueries.getUsersForRole(this, tenantIdentifier, role); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1746,8 +1747,7 @@ public String[] getUsersForRole(TenantIdentifier tenantIdentifier, String role) @Override public String[] getPermissionsForRole(AppIdentifier appIdentifier, String role) throws StorageQueryException { try { - // TODO.. - return UserRolesQueries.getPermissionsForRole(this, role); + return UserRolesQueries.getPermissionsForRole(this, appIdentifier, role); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1757,8 +1757,7 @@ public String[] getPermissionsForRole(AppIdentifier appIdentifier, String role) public String[] getRolesThatHavePermission(AppIdentifier appIdentifier, String permission) throws StorageQueryException { try { - // TODO.. - return UserRolesQueries.getRolesThatHavePermission(this, permission); + return UserRolesQueries.getRolesThatHavePermission(this, appIdentifier, permission); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1767,8 +1766,7 @@ public String[] getRolesThatHavePermission(AppIdentifier appIdentifier, String p @Override public boolean deleteRole(AppIdentifier appIdentifier, String role) throws StorageQueryException { try { - // TODO.. - return UserRolesQueries.deleteRole(this, role); + return UserRolesQueries.deleteRole(this, appIdentifier, role); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1777,8 +1775,7 @@ public boolean deleteRole(AppIdentifier appIdentifier, String role) throws Stora @Override public String[] getRoles(AppIdentifier appIdentifier) throws StorageQueryException { try { - // TODO.. - return UserRolesQueries.getRoles(this); + return UserRolesQueries.getRoles(this, appIdentifier); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1787,8 +1784,7 @@ public String[] getRoles(AppIdentifier appIdentifier) throws StorageQueryExcepti @Override public boolean doesRoleExist(AppIdentifier appIdentifier, String role) throws StorageQueryException { try { - // TODO.. - return UserRolesQueries.doesRoleExist(this, role); + return UserRolesQueries.doesRoleExist(this, appIdentifier, role); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1797,8 +1793,7 @@ public boolean doesRoleExist(AppIdentifier appIdentifier, String role) throws St @Override public int deleteAllRolesForUser(TenantIdentifier tenantIdentifier, String userId) throws StorageQueryException { try { - // TODO.. - return UserRolesQueries.deleteAllRolesForUser(this, userId); + return UserRolesQueries.deleteAllRolesForUser(this, tenantIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1807,8 +1802,7 @@ public int deleteAllRolesForUser(TenantIdentifier tenantIdentifier, String userI @Override public void deleteAllRolesForUser(AppIdentifier appIdentifier, String userId) throws StorageQueryException { try { - // TODO.. - UserRolesQueries.deleteAllRolesForUser(this, userId); + UserRolesQueries.deleteAllRolesForUser(this, appIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1818,26 +1812,33 @@ public void deleteAllRolesForUser(AppIdentifier appIdentifier, String userId) th public boolean deleteRoleForUser_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, String userId, String role) throws StorageQueryException { - // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { - return UserRolesQueries.deleteRoleForUser_Transaction(this, sqlCon, userId, role); + return UserRolesQueries.deleteRoleForUser_Transaction(this, sqlCon, tenantIdentifier, userId, role); } catch (SQLException e) { throw new StorageQueryException(e); } } @Override - public boolean createNewRoleOrDoNothingIfExists_Transaction(TenantIdentifier tenantIdentifier, + public boolean createNewRoleOrDoNothingIfExists_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String role) - throws StorageQueryException { - // TODO.. + throws StorageQueryException, TenantOrAppNotFoundException { Connection sqlCon = (Connection) con.getConnection(); try { - return UserRolesQueries.createNewRoleOrDoNothingIfExists_Transaction(this, sqlCon, role); + return UserRolesQueries.createNewRoleOrDoNothingIfExists_Transaction( + this, sqlCon, appIdentifier, role); } catch (SQLException e) { + if (e instanceof PSQLException) { + PostgreSQLConfig config = Config.getConfig(this); + ServerErrorMessage serverMessage = ((PSQLException) e).getServerErrorMessage(); + + if (isForeignKeyConstraintError(serverMessage, config.getRolesTable(), "app_id")) { + throw new TenantOrAppNotFoundException(appIdentifier); + } + } throw new StorageQueryException(e); } } @@ -1847,10 +1848,10 @@ public void addPermissionToRoleOrDoNothingIfExists_Transaction(AppIdentifier app TransactionConnection con, String role, String permission) throws StorageQueryException, UnknownRoleException { - // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { - UserRolesQueries.addPermissionToRoleOrDoNothingIfExists_Transaction(this, sqlCon, role, permission); + UserRolesQueries.addPermissionToRoleOrDoNothingIfExists_Transaction(this, sqlCon, appIdentifier, + role, permission); } catch (SQLException e) { if (e instanceof PSQLException) { PostgreSQLConfig config = Config.getConfig(this); @@ -1868,10 +1869,9 @@ public void addPermissionToRoleOrDoNothingIfExists_Transaction(AppIdentifier app public boolean deletePermissionForRole_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String role, String permission) throws StorageQueryException { - // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { - return UserRolesQueries.deletePermissionForRole_Transaction(this, sqlCon, role, permission); + return UserRolesQueries.deletePermissionForRole_Transaction(this, sqlCon, appIdentifier, role, permission); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1881,10 +1881,9 @@ public boolean deletePermissionForRole_Transaction(AppIdentifier appIdentifier, public int deleteAllPermissionsForRole_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String role) throws StorageQueryException { - // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { - return UserRolesQueries.deleteAllPermissionsForRole_Transaction(this, sqlCon, role); + return UserRolesQueries.deleteAllPermissionsForRole_Transaction(this, sqlCon, appIdentifier, role); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1893,10 +1892,9 @@ public int deleteAllPermissionsForRole_Transaction(AppIdentifier appIdentifier, @Override public boolean doesRoleExist_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String role) throws StorageQueryException { - // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { - return UserRolesQueries.doesRoleExist_transaction(this, sqlCon, role); + return UserRolesQueries.doesRoleExist_transaction(this, sqlCon, appIdentifier, role); } catch (SQLException e) { throw new StorageQueryException(e); } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/UserRolesQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/UserRolesQueries.java index e1b5e280..928b5e92 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/UserRolesQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/UserRolesQueries.java @@ -17,7 +17,9 @@ package io.supertokens.storage.postgresql.queries; import io.supertokens.pluginInterface.exceptions.StorageQueryException; -import io.supertokens.storage.postgresql.PreparedStatementValueSetter; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.config.Config; import io.supertokens.storage.postgresql.utils.Utils; @@ -37,8 +39,14 @@ public static String getQueryToCreateRolesTable(Start start) { String tableName = getConfig(start).getRolesTable(); // @formatter:off return "CREATE TABLE IF NOT EXISTS " + tableName + " (" + + "app_id VARCHAR(64) DEFAULT 'public'," + "role VARCHAR(255) NOT NULL," - + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, null, "pkey") + " PRIMARY KEY(role)" + " );"; + + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, null, "pkey") + + " PRIMARY KEY(app_id, role)," + + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, "app_id", "fkey") + + " FOREIGN KEY(app_id)" + + " REFERENCES " + Config.getConfig(start).getAppsTable() + " (app_id) ON DELETE CASCADE" + + ");"; // @formatter:on } @@ -48,19 +56,21 @@ public static String getQueryToCreateRolePermissionsTable(Start start) { String schema = Config.getConfig(start).getTableSchema(); // @formatter:off return "CREATE TABLE IF NOT EXISTS " + tableName + " (" + + "app_id VARCHAR(64) DEFAULT 'public'," + "role VARCHAR(255) NOT NULL," + "permission VARCHAR(255) NOT NULL," - + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, null, "pkey") + " PRIMARY KEY(role, permission)," - + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, "role", "fkey") + " FOREIGN KEY(role)" - + " REFERENCES " + getConfig(start).getRolesTable() - +"(role) ON DELETE CASCADE );"; - + + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, null, "pkey") + + " PRIMARY KEY(app_id, role, permission)," + + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, "role", "fkey") + + " FOREIGN KEY(app_id, role)" + + " REFERENCES " + getConfig(start).getRolesTable() + "(app_id, role) ON DELETE CASCADE" + + ");"; // @formatter:on } static String getQueryToCreateRolePermissionsPermissionIndex(Start start) { return "CREATE INDEX role_permissions_permission_index ON " + getConfig(start).getUserRolesPermissionsTable() - + "(permission);"; + + "(app_id, permission);"; } public static String getQueryToCreateUserRolesTable(Start start) { @@ -68,54 +78,77 @@ public static String getQueryToCreateUserRolesTable(Start start) { String tableName = getConfig(start).getUserRolesTable(); // @formatter:off return "CREATE TABLE IF NOT EXISTS " + tableName + " (" + + "app_id VARCHAR(64) DEFAULT 'public'," + + "tenant_id VARCHAR(64) DEFAULT 'public'," + "user_id VARCHAR(128) NOT NULL," + "role VARCHAR(255) NOT NULL," - + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, null, "pkey") + " PRIMARY KEY(user_id, role)," - + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, "role", "fkey") + " FOREIGN KEY(role)" - + " REFERENCES " + getConfig(start).getRolesTable() - +"(role) ON DELETE CASCADE );"; - + + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, null, "pkey") + + " PRIMARY KEY(app_id, tenant_id, user_id, role)," + + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, "role", "fkey") + + " FOREIGN KEY(app_id, role)" + + " REFERENCES " + getConfig(start).getRolesTable() + "(app_id, role) ON DELETE CASCADE," + + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, "tenant_id", "fkey") + + " FOREIGN KEY (app_id, tenant_id)" + + " REFERENCES " + Config.getConfig(start).getTenantsTable() + "(app_id, tenant_id) ON DELETE CASCADE" + + ");"; // @formatter:on } public static String getQueryToCreateUserRolesRoleIndex(Start start) { - return "CREATE INDEX user_roles_role_index ON " + getConfig(start).getUserRolesTable() + "(role);"; + return "CREATE INDEX user_roles_role_index ON " + getConfig(start).getUserRolesTable() + + "(app_id, tenant_id, role);"; } - public static boolean createNewRoleOrDoNothingIfExists_Transaction(Start start, Connection con, String role) + public static boolean createNewRoleOrDoNothingIfExists_Transaction(Start start, Connection con, + AppIdentifier appIdentifier, String role) throws SQLException, StorageQueryException { - String QUERY = "INSERT INTO " + getConfig(start).getRolesTable() + " VALUES(?) ON CONFLICT DO NOTHING;"; - int rowsUpdated = update(con, QUERY, pst -> pst.setString(1, role)); + String QUERY = "INSERT INTO " + getConfig(start).getRolesTable() + + "(app_id, role) VALUES (?, ?) ON CONFLICT DO NOTHING;"; + int rowsUpdated = update(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, role); + }); return rowsUpdated > 0; } - public static void addPermissionToRoleOrDoNothingIfExists_Transaction(Start start, Connection con, String role, - String permission) throws SQLException, StorageQueryException { + public static void addPermissionToRoleOrDoNothingIfExists_Transaction(Start start, Connection con, + AppIdentifier appIdentifier, String role, + String permission) throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + getConfig(start).getUserRolesPermissionsTable() - + " (role, permission) VALUES(?, ?) ON CONFLICT DO NOTHING"; + + " (app_id, role, permission) VALUES(?, ?, ?) ON CONFLICT DO NOTHING"; update(con, QUERY, pst -> { - pst.setString(1, role); - pst.setString(2, permission); + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, role); + pst.setString(3, permission); }); } - public static boolean deleteRole(Start start, String role) throws SQLException, StorageQueryException { - String QUERY = "DELETE FROM " + getConfig(start).getRolesTable() + " WHERE role = ? ;"; + public static boolean deleteRole(Start start, AppIdentifier appIdentifier, String role) throws SQLException, StorageQueryException { + String QUERY = "DELETE FROM " + getConfig(start).getRolesTable() + + " WHERE app_id = ? AND role = ? ;"; return update(start, QUERY, pst -> { - pst.setString(1, role); + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, role); }) == 1; } - public static boolean doesRoleExist(Start start, String role) throws SQLException, StorageQueryException { - String QUERY = "SELECT 1 FROM " + getConfig(start).getRolesTable() + " WHERE role = ?"; - return execute(start, QUERY, pst -> pst.setString(1, role), ResultSet::next); + public static boolean doesRoleExist(Start start, AppIdentifier appIdentifier, String role) throws SQLException, StorageQueryException { + String QUERY = "SELECT 1 FROM " + getConfig(start).getRolesTable() + + " WHERE app_id = ? AND role = ?"; + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, role); + }, ResultSet::next); } - public static String[] getPermissionsForRole(Start start, String role) throws SQLException, StorageQueryException { + public static String[] getPermissionsForRole(Start start, AppIdentifier appIdentifier, String role) throws SQLException, StorageQueryException { String QUERY = "SELECT permission FROM " + Config.getConfig(start).getUserRolesPermissionsTable() - + " WHERE role = ?;"; - return execute(start, QUERY, pst -> pst.setString(1, role), result -> { + + " WHERE app_id = ? AND role = ?;"; + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, role); + }, result -> { ArrayList permissions = new ArrayList<>(); while (result.next()) { permissions.add(result.getString("permission")); @@ -124,9 +157,9 @@ public static String[] getPermissionsForRole(Start start, String role) throws SQ }); } - public static String[] getRoles(Start start) throws SQLException, StorageQueryException { - String QUERY = "SELECT role FROM " + getConfig(start).getRolesTable(); - return execute(start, QUERY, PreparedStatementValueSetter.NO_OP_SETTER, result -> { + public static String[] getRoles(Start start, AppIdentifier appIdentifier) throws SQLException, StorageQueryException { + String QUERY = "SELECT role FROM " + getConfig(start).getRolesTable() + " WHERE app_id = ?"; + return execute(start, QUERY, pst -> pst.setString(1, appIdentifier.getAppId()), result -> { ArrayList roles = new ArrayList<>(); while (result.next()) { roles.add(result.getString("role")); @@ -135,19 +168,45 @@ public static String[] getRoles(Start start) throws SQLException, StorageQueryEx }); } - public static int addRoleToUser(Start start, String userId, String role) + public static int addRoleToUser(Start start, TenantIdentifier tenantIdentifier, String userId, String role) throws SQLException, StorageQueryException { - String QUERY = "INSERT INTO " + getConfig(start).getUserRolesTable() + "(user_id, role) VALUES(?, ?);"; + String QUERY = "INSERT INTO " + getConfig(start).getUserRolesTable() + + "(app_id, tenant_id, user_id, role) VALUES(?, ?, ?, ?);"; return update(start, QUERY, pst -> { - pst.setString(1, userId); - pst.setString(2, role); + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, userId); + pst.setString(4, role); + }); + } + + public static String[] getRolesForUser(Start start, TenantIdentifier tenantIdentifier, String userId) + throws SQLException, StorageQueryException { + String QUERY = "SELECT role FROM " + getConfig(start).getUserRolesTable() + + " WHERE app_id = ? AND tenant_id = ? AND user_id = ? ;"; + + return execute(start, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, userId); + }, result -> { + ArrayList roles = new ArrayList<>(); + while (result.next()) { + roles.add(result.getString("role")); + } + return roles.toArray(String[]::new); }); } - public static String[] getRolesForUser(Start start, String userId) throws SQLException, StorageQueryException { - String QUERY = "SELECT role FROM " + getConfig(start).getUserRolesTable() + " WHERE user_id = ? ;"; + public static String[] getRolesForUser(Start start, AppIdentifier appIdentifier, String userId) + throws SQLException, StorageQueryException { + String QUERY = "SELECT role FROM " + getConfig(start).getUserRolesTable() + + " WHERE app_id = ? AND user_id = ? ;"; - return execute(start, QUERY, pst -> pst.setString(1, userId), result -> { + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }, result -> { ArrayList roles = new ArrayList<>(); while (result.next()) { roles.add(result.getString("role")); @@ -156,27 +215,40 @@ public static String[] getRolesForUser(Start start, String userId) throws SQLExc }); } - public static boolean deleteRoleForUser_Transaction(Start start, Connection con, String userId, String role) + public static boolean deleteRoleForUser_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, + String userId, String role) throws SQLException, StorageQueryException { - String QUERY = "DELETE FROM " + getConfig(start).getUserRolesTable() + " WHERE user_id = ? AND role = ? ;"; + String QUERY = "DELETE FROM " + getConfig(start).getUserRolesTable() + + " WHERE app_id = ? AND tenant_id = ? AND user_id = ? AND role = ? ;"; // store the number of rows updated int rowUpdatedCount = update(con, QUERY, pst -> { - pst.setString(1, userId); - pst.setString(2, role); + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, userId); + pst.setString(4, role); }); return rowUpdatedCount > 0; } - public static boolean doesRoleExist_transaction(Start start, Connection con, String role) + public static boolean doesRoleExist_transaction(Start start, Connection con, AppIdentifier appIdentifier, String role) throws SQLException, StorageQueryException { - String QUERY = "SELECT 1 FROM " + getConfig(start).getRolesTable() + " WHERE role = ? FOR UPDATE"; - return execute(con, QUERY, pst -> pst.setString(1, role), ResultSet::next); + String QUERY = "SELECT 1 FROM " + getConfig(start).getRolesTable() + + " WHERE app_id = ? AND role = ? FOR UPDATE"; + return execute(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, role); + }, ResultSet::next); } - public static String[] getUsersForRole(Start start, String role) throws SQLException, StorageQueryException { - String QUERY = "SELECT user_id FROM " + getConfig(start).getUserRolesTable() + " WHERE role = ? "; - return execute(start, QUERY, pst -> pst.setString(1, role), result -> { + public static String[] getUsersForRole(Start start, TenantIdentifier tenantIdentifier, String role) throws SQLException, StorageQueryException { + String QUERY = "SELECT user_id FROM " + getConfig(start).getUserRolesTable() + + " WHERE app_id = ? AND tenant_id = ? AND role = ? "; + return execute(start, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, role); + }, result -> { ArrayList userIds = new ArrayList<>(); while (result.next()) { userIds.add(result.getString("user_id")); @@ -185,37 +257,46 @@ public static String[] getUsersForRole(Start start, String role) throws SQLExcep }); } - public static boolean deletePermissionForRole_Transaction(Start start, Connection con, String role, - String permission) throws SQLException, StorageQueryException { + public static boolean deletePermissionForRole_Transaction(Start start, Connection con, AppIdentifier appIdentifier, + String role, + String permission) throws SQLException, StorageQueryException { String QUERY = "DELETE FROM " + getConfig(start).getUserRolesPermissionsTable() - + " WHERE role = ? AND permission = ? "; + + " WHERE app_id = ? AND role = ? AND permission = ? "; // store the number of rows updated int rowUpdatedCount = update(con, QUERY, pst -> { - pst.setString(1, role); - pst.setString(2, permission); + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, role); + pst.setString(3, permission); }); return rowUpdatedCount > 0; } - public static int deleteAllPermissionsForRole_Transaction(Start start, Connection con, String role) + public static int deleteAllPermissionsForRole_Transaction(Start start, Connection con, AppIdentifier appIdentifier, + String role) throws SQLException, StorageQueryException { - String QUERY = "DELETE FROM " + getConfig(start).getUserRolesPermissionsTable() + " WHERE role = ? "; + String QUERY = "DELETE FROM " + getConfig(start).getUserRolesPermissionsTable() + + " WHERE app_id = ? AND role = ? "; // return the number of rows updated return update(con, QUERY, pst -> { - pst.setString(1, role); + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, role); }); } - public static String[] getRolesThatHavePermission(Start start, String permission) + public static String[] getRolesThatHavePermission(Start start, AppIdentifier appIdentifier, String permission) throws SQLException, StorageQueryException { - String QUERY = "SELECT role FROM " + getConfig(start).getUserRolesPermissionsTable() + " WHERE permission = ? "; + String QUERY = "SELECT role FROM " + getConfig(start).getUserRolesPermissionsTable() + + " WHERE app_id = ? AND permission = ? "; - return execute(start, QUERY, pst -> pst.setString(1, permission), result -> { + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, permission); + }, result -> { ArrayList roles = new ArrayList<>(); while (result.next()) { @@ -226,9 +307,22 @@ public static String[] getRolesThatHavePermission(Start start, String permission }); } - public static int deleteAllRolesForUser(Start start, String userId) throws SQLException, StorageQueryException { - String QUERY = "DELETE FROM " + getConfig(start).getUserRolesTable() + " WHERE user_id = ?"; - return update(start, QUERY, pst -> pst.setString(1, userId)); + public static int deleteAllRolesForUser(Start start, TenantIdentifier tenantIdentifier, String userId) throws SQLException, StorageQueryException { + String QUERY = "DELETE FROM " + getConfig(start).getUserRolesTable() + + " WHERE app_id = ? AND tenant_id = ? AND user_id = ?"; + return update(start, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, userId); + }); } + public static int deleteAllRolesForUser(Start start, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { + String QUERY = "DELETE FROM " + getConfig(start).getUserRolesTable() + + " WHERE app_id = ? AND user_id = ?"; + return update(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); + } } From d282b12bc7b2f39c7bd29e5b1a33f92e4bfab609 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Wed, 29 Mar 2023 11:15:40 +0530 Subject: [PATCH 045/106] fix: Multitenant usermetadata (#70) * fix: user roles impl * fix: handling fkey * fix: usermetadata impl * fix: transaction fix * fix: transaction fix --- .../supertokens/storage/postgresql/Start.java | 31 ++++++++---- .../queries/UserMetadataQueries.java | 50 +++++++++++++------ 2 files changed, 56 insertions(+), 25 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index e122eb79..e5523ee0 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -705,10 +705,17 @@ public void addInfoToNonAuthRecipesBasedOnUserId(String className, String userId data.addProperty("test", "testData"); try { this.startTransaction(con -> { - setUserMetadata_Transaction(new AppIdentifier(null, null), con, userId, data); + try { + setUserMetadata_Transaction(new AppIdentifier(null, null), con, userId, data); + } catch (TenantOrAppNotFoundException e) { + throw new StorageTransactionLogicException(e); + } return null; }); } catch (StorageTransactionLogicException e) { + if (e.actualException instanceof TenantOrAppNotFoundException) { + throw new IllegalStateException(e); + } throw new StorageQueryException(e); } } else if (className.equals(JWTRecipeStorage.class.getName())) { @@ -1651,8 +1658,7 @@ public io.supertokens.pluginInterface.passwordless.UserInfo getUserByPhoneNumber @Override public JsonObject getUserMetadata(AppIdentifier appIdentifier, String userId) throws StorageQueryException { try { - // TODO.. - return UserMetadataQueries.getUserMetadata(this, userId); + return UserMetadataQueries.getUserMetadata(this, appIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1661,10 +1667,9 @@ public JsonObject getUserMetadata(AppIdentifier appIdentifier, String userId) th @Override public JsonObject getUserMetadata_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId) throws StorageQueryException { - // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { - return UserMetadataQueries.getUserMetadata_Transaction(this, sqlCon, userId); + return UserMetadataQueries.getUserMetadata_Transaction(this, sqlCon, appIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1673,12 +1678,19 @@ public JsonObject getUserMetadata_Transaction(AppIdentifier appIdentifier, Trans @Override public int setUserMetadata_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId, JsonObject metadata) - throws StorageQueryException { - // TODO.. + throws StorageQueryException, TenantOrAppNotFoundException { Connection sqlCon = (Connection) con.getConnection(); try { - return UserMetadataQueries.setUserMetadata_Transaction(this, sqlCon, userId, metadata); + return UserMetadataQueries.setUserMetadata_Transaction(this, sqlCon, appIdentifier, userId, metadata); } catch (SQLException e) { + if (e instanceof PSQLException) { + PostgreSQLConfig config = Config.getConfig(this); + ServerErrorMessage serverMessage = ((PSQLException) e).getServerErrorMessage(); + + if (isForeignKeyConstraintError(serverMessage, config.getUserMetadataTable(), "app_id")) { + throw new TenantOrAppNotFoundException(appIdentifier); + } + } throw new StorageQueryException(e); } } @@ -1686,8 +1698,7 @@ public int setUserMetadata_Transaction(AppIdentifier appIdentifier, TransactionC @Override public int deleteUserMetadata(AppIdentifier appIdentifier, String userId) throws StorageQueryException { try { - // TODO.. - return UserMetadataQueries.deleteUserMetadata(this, userId); + return UserMetadataQueries.deleteUserMetadata(this, appIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/UserMetadataQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/UserMetadataQueries.java index 301aab6f..f4c5d161 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/UserMetadataQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/UserMetadataQueries.java @@ -20,6 +20,7 @@ import com.google.gson.JsonParser; import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.config.Config; import io.supertokens.storage.postgresql.utils.Utils; @@ -38,37 +39,52 @@ public static String getQueryToCreateUserMetadataTable(Start start) { String tableName = Config.getConfig(start).getUserMetadataTable(); // @formatter:off return "CREATE TABLE IF NOT EXISTS " + tableName + " (" + + "app_id VARCHAR(64) DEFAULT 'public'," + "user_id VARCHAR(128) NOT NULL," + "user_metadata TEXT NOT NULL," - + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, null, "pkey") + " PRIMARY KEY(user_id)" + " );"; + + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, null, "pkey") + + " PRIMARY KEY(app_id, user_id)," + + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, "app_id", "fkey") + + " FOREIGN KEY(app_id)" + + " REFERENCES " + Config.getConfig(start).getAppsTable() + " (app_id) ON DELETE CASCADE" + + ");"; // @formatter:on } - public static int deleteUserMetadata(Start start, String userId) throws SQLException, StorageQueryException { - String QUERY = "DELETE FROM " + getConfig(start).getUserMetadataTable() + " WHERE user_id = ?"; + public static int deleteUserMetadata(Start start, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { + String QUERY = "DELETE FROM " + getConfig(start).getUserMetadataTable() + + " WHERE app_id = ? AND user_id = ?"; - return update(start, QUERY.toString(), pst -> pst.setString(1, userId)); + return update(start, QUERY.toString(), pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); } - public static int setUserMetadata_Transaction(Start start, Connection con, String userId, JsonObject metadata) + public static int setUserMetadata_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String userId, JsonObject metadata) throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + getConfig(start).getUserMetadataTable() - + "(user_id, user_metadata) VALUES(?, ?) " - + "ON CONFLICT(user_id) DO UPDATE SET user_metadata=excluded.user_metadata;"; + + "(app_id, user_id, user_metadata) VALUES(?, ?, ?) " + + "ON CONFLICT(app_id, user_id) DO UPDATE SET user_metadata=excluded.user_metadata;"; return update(con, QUERY, pst -> { - pst.setString(1, userId); - pst.setString(2, metadata.toString()); + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + pst.setString(3, metadata.toString()); }); } - public static JsonObject getUserMetadata_Transaction(Start start, Connection con, String userId) + public static JsonObject getUserMetadata_Transaction(Start start, Connection con, AppIdentifier appIdentifier, + String userId) throws SQLException, StorageQueryException { String QUERY = "SELECT user_metadata FROM " + getConfig(start).getUserMetadataTable() - + " WHERE user_id = ? FOR UPDATE"; - return execute(con, QUERY, pst -> pst.setString(1, userId), result -> { + + " WHERE app_id = ? AND user_id = ? FOR UPDATE"; + return execute(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }, result -> { if (result.next()) { JsonParser jp = new JsonParser(); return jp.parse(result.getString("user_metadata")).getAsJsonObject(); @@ -77,9 +93,13 @@ public static JsonObject getUserMetadata_Transaction(Start start, Connection con }); } - public static JsonObject getUserMetadata(Start start, String userId) throws SQLException, StorageQueryException { - String QUERY = "SELECT user_metadata FROM " + getConfig(start).getUserMetadataTable() + " WHERE user_id = ?"; - return execute(start, QUERY, pst -> pst.setString(1, userId), result -> { + public static JsonObject getUserMetadata(Start start, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { + String QUERY = "SELECT user_metadata FROM " + getConfig(start).getUserMetadataTable() + + " WHERE app_id = ? AND user_id = ?"; + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }, result -> { if (result.next()) { JsonParser jp = new JsonParser(); return jp.parse(result.getString("user_metadata")).getAsJsonObject(); From 7a9adbc4d572f8c4af99bc45ebf6f96121f6331b Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Wed, 29 Mar 2023 12:16:55 +0530 Subject: [PATCH 046/106] fix: ep storage (#71) --- .../storage/postgresql/test/ExceptionParsingTest.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java b/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java index a71d77c2..7e2a6242 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java @@ -25,6 +25,7 @@ import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicatePasswordResetTokenException; import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateUserIdException; import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; +import io.supertokens.pluginInterface.emailpassword.sqlStorage.EmailPasswordSQLStorage; import io.supertokens.pluginInterface.emailverification.EmailVerificationTokenInfo; import io.supertokens.pluginInterface.emailverification.exception.DuplicateEmailVerificationTokenException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; @@ -120,7 +121,7 @@ public void emailPasswordSignupExceptions() throws Exception { TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - var storage = StorageLayer.getEmailPasswordStorage(process.getProcess()); + EmailPasswordSQLStorage storage = (EmailPasswordSQLStorage) StorageLayer.getStorage(process.getProcess()); String userId = "userId"; String userId2 = "userId2"; @@ -164,7 +165,7 @@ public void updateUsersEmail_TransactionExceptions() TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - var storage = StorageLayer.getEmailPasswordStorage(process.getProcess()); + EmailPasswordSQLStorage storage = (EmailPasswordSQLStorage) StorageLayer.getStorage(process.getProcess()); String userId = "userId"; String userId2 = "userId2"; @@ -277,7 +278,7 @@ public void addPasswordResetTokenExceptions() throws Exception { TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - var storage = StorageLayer.getEmailPasswordStorage(process.getProcess()); + EmailPasswordSQLStorage storage = (EmailPasswordSQLStorage) StorageLayer.getStorage(process.getProcess()); String userId = "userId"; String tokenHash = "fakehash"; @@ -338,7 +339,7 @@ public void verifyEmailExceptions() throws Exception { TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - var storage = StorageLayer.getEmailPasswordStorage(process.getProcess()); + EmailPasswordSQLStorage storage = (EmailPasswordSQLStorage) StorageLayer.getStorage(process.getProcess()); String userId = "userId"; String userId2 = "userId2"; From 09729e731e2aaf350f72936a35da4757104d52dc Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Fri, 31 Mar 2023 17:52:30 +0530 Subject: [PATCH 047/106] fix: thirdparty storage (#74) --- .../storage/postgresql/test/ExceptionParsingTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java b/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java index 7e2a6242..2d140167 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java @@ -37,6 +37,7 @@ import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.thirdparty.exception.DuplicateThirdPartyUserException; +import io.supertokens.pluginInterface.thirdparty.sqlStorage.ThirdPartySQLStorage; import io.supertokens.storageLayer.StorageLayer; import org.junit.AfterClass; import org.junit.Before; @@ -78,7 +79,7 @@ public void thirdPartySignupExceptions() throws Exception { TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - var storage = StorageLayer.getThirdPartyStorage(process.getProcess()); + ThirdPartySQLStorage storage = (ThirdPartySQLStorage) StorageLayer.getStorage(process.getProcess()); String userId = "userId"; String userId2 = "userId2"; From 7eb9f2affd7f376bb3e4bcafdef8026f32edca26 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Mon, 3 Apr 2023 11:08:28 +0530 Subject: [PATCH 048/106] fix: Multitenant thirdparty changes for update email (#75) * fix: thirdparty storage * fix: thirdparty changes * fix: pr comments --- .../supertokens/storage/postgresql/Start.java | 8 ++--- .../postgresql/queries/GeneralQueries.java | 1 + .../postgresql/queries/ThirdPartyQueries.java | 35 ++++++++----------- 3 files changed, 20 insertions(+), 24 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index e5523ee0..ecbc250c 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -1052,13 +1052,13 @@ public void deleteExpiredPasswordResetTokens() throws StorageQueryException { @Override public io.supertokens.pluginInterface.thirdparty.UserInfo getUserInfoUsingId_Transaction( - TenantIdentifier tenantIdentifier, TransactionConnection con, + AppIdentifier appIdentifier, TransactionConnection con, String thirdPartyId, String thirdPartyUserId) throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { - return ThirdPartyQueries.getUserInfoUsingId_Transaction(this, sqlCon, tenantIdentifier, thirdPartyId, + return ThirdPartyQueries.getUserInfoUsingId_Transaction(this, sqlCon, appIdentifier, thirdPartyId, thirdPartyUserId); } catch (SQLException e) { throw new StorageQueryException(e); @@ -1066,12 +1066,12 @@ public io.supertokens.pluginInterface.thirdparty.UserInfo getUserInfoUsingId_Tra } @Override - public void updateUserEmail_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, + public void updateUserEmail_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String thirdPartyId, String thirdPartyUserId, String newEmail) throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { - ThirdPartyQueries.updateUserEmail_Transaction(this, sqlCon, tenantIdentifier, thirdPartyId, + ThirdPartyQueries.updateUserEmail_Transaction(this, sqlCon, appIdentifier, thirdPartyId, thirdPartyUserId, newEmail); } catch (SQLException e) { throw new StorageQueryException(e); diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java index b6321809..c444feee 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java @@ -247,6 +247,7 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto update(start, ThirdPartyQueries.getQueryToCreateUsersTable(start), NO_OP_SETTER); // index update(start, ThirdPartyQueries.getQueryToThirdPartyUserEmailIndex(start), NO_OP_SETTER); + update(start, ThirdPartyQueries.getQueryToThirdPartyUserIdIndex(start), NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getThirdPartyUserToTenantTable())) { diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/ThirdPartyQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/ThirdPartyQueries.java index cb73b846..3339cde0 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/ThirdPartyQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/ThirdPartyQueries.java @@ -67,6 +67,11 @@ public static String getQueryToThirdPartyUserEmailIndex(Start start) { + Config.getConfig(start).getThirdPartyUsersTable() + " (app_id, email);"; } + public static String getQueryToThirdPartyUserIdIndex(Start start) { + return "CREATE INDEX IF NOT EXISTS thirdparty_users_thirdparty_user_id_index ON " + + Config.getConfig(start).getThirdPartyUsersTable() + " (app_id, third_party_id, third_party_user_id);"; + } + static String getQueryToCreateThirdPartyUserToTenantTable(Start start) { String schema = Config.getConfig(start).getTableSchema(); String thirdPartyUserToTenantTable = Config.getConfig(start).getThirdPartyUserToTenantTable(); @@ -248,42 +253,32 @@ public static UserInfo getThirdPartyUserInfoUsingId(Start start, TenantIdentifie }); } - public static void updateUserEmail_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, + public static void updateUserEmail_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String thirdPartyId, String thirdPartyUserId, String newEmail) throws SQLException, StorageQueryException { String QUERY = "UPDATE " + getConfig(start).getThirdPartyUsersTable() - + " SET email = ? WHERE app_id = ? AND user_id IN (" - + " SELECT user_id FROM " + getConfig(start).getThirdPartyUserToTenantTable() - + " WHERE app_id = ? AND tenant_id = ? AND third_party_id = ? AND third_party_user_id = ?" - + ")"; + + " SET email = ? WHERE app_id = ? AND third_party_id = ? AND third_party_user_id = ?"; update(con, QUERY, pst -> { pst.setString(1, newEmail); - pst.setString(2, tenantIdentifier.getAppId()); - pst.setString(3, tenantIdentifier.getAppId()); - pst.setString(4, tenantIdentifier.getTenantId()); - pst.setString(5, thirdPartyId); - pst.setString(6, thirdPartyUserId); + pst.setString(2, appIdentifier.getAppId()); + pst.setString(3, thirdPartyId); + pst.setString(4, thirdPartyUserId); }); } public static UserInfo getUserInfoUsingId_Transaction(Start start, Connection con, - TenantIdentifier tenantIdentifier, String thirdPartyId, + AppIdentifier appIdentifier, String thirdPartyId, String thirdPartyUserId) throws SQLException, StorageQueryException { String QUERY = "SELECT user_id, third_party_id, third_party_user_id, email, time_joined FROM " + getConfig(start).getThirdPartyUsersTable() - + " WHERE app_id = ? AND user_id IN (" - + " SELECT user_id FROM " + getConfig(start).getThirdPartyUserToTenantTable() - + " WHERE app_id = ? AND tenant_id = ? AND third_party_id = ? AND third_party_user_id = ?" - + ") FOR UPDATE"; + + " WHERE app_id = ? AND third_party_id = ? AND third_party_user_id = ? FOR UPDATE"; return execute(con, QUERY, pst -> { - pst.setString(1, tenantIdentifier.getAppId()); - pst.setString(2, tenantIdentifier.getAppId()); - pst.setString(3, tenantIdentifier.getTenantId()); - pst.setString(4, thirdPartyId); - pst.setString(5, thirdPartyUserId); + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, thirdPartyId); + pst.setString(3, thirdPartyUserId); }, result -> { if (result.next()) { return UserInfoRowMapper.getInstance().mapOrThrow(result); From 8dc347c2c3a0224316fd2b9a62f3aedb4626886d Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Mon, 3 Apr 2023 14:26:15 +0530 Subject: [PATCH 049/106] fix: Multitenant emailverification storage (#76) * fix: thirdparty storage * fix: emailverification storage --- .../storage/postgresql/test/ExceptionParsingTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java b/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java index 2d140167..4de14b77 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java @@ -28,6 +28,7 @@ import io.supertokens.pluginInterface.emailpassword.sqlStorage.EmailPasswordSQLStorage; import io.supertokens.pluginInterface.emailverification.EmailVerificationTokenInfo; import io.supertokens.pluginInterface.emailverification.exception.DuplicateEmailVerificationTokenException; +import io.supertokens.pluginInterface.emailverification.sqlStorage.EmailVerificationSQLStorage; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.jwt.JWTSymmetricSigningKeyInfo; @@ -217,7 +218,7 @@ public void updateIsEmailVerified_TransactionExceptions() TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - var storage = StorageLayer.getEmailVerificationStorage(process.getProcess()); + EmailVerificationSQLStorage storage = (EmailVerificationSQLStorage) StorageLayer.getStorage(process.getProcess()); String userId = "userId"; String userEmail = "useremail@asdf.fdas"; @@ -313,7 +314,7 @@ public void addEmailVerificationTokenExceptions() throws Exception { TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - var storage = StorageLayer.getEmailVerificationStorage(process.getProcess()); + EmailVerificationSQLStorage storage = (EmailVerificationSQLStorage) StorageLayer.getStorage(process.getProcess()); String userId = "userId"; String tokenHash = "fakehash"; From 105b5f0e68d9d7ac25fd0c6ca818adf662e77692 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Mon, 3 Apr 2023 17:10:18 +0530 Subject: [PATCH 050/106] fix: tokens tenant specific (#77) --- .../supertokens/storage/postgresql/Start.java | 35 ++++---- .../queries/EmailVerificationQueries.java | 80 +++++++++++-------- .../postgresql/test/ExceptionParsingTest.java | 4 +- 3 files changed, 65 insertions(+), 54 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index ecbc250c..11c1ef65 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -693,7 +693,7 @@ public void addInfoToNonAuthRecipesBasedOnUserId(String className, String userId try { EmailVerificationTokenInfo info = new EmailVerificationTokenInfo(userId, "someToken", 10000, "test123@example.com"); - addEmailVerificationToken(new AppIdentifier(null, null), info); + addEmailVerificationToken(new TenantIdentifier(null, null, null), info); } catch (DuplicateEmailVerificationTokenException e) { throw new StorageQueryException(e); @@ -906,26 +906,27 @@ public void deleteExpiredEmailVerificationTokens() throws StorageQueryException } @Override - public EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUser_Transaction(AppIdentifier appIdentifier, - TransactionConnection con, - String userId, String email) + public EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUser_Transaction( + TenantIdentifier tenantIdentifier, + TransactionConnection con, + String userId, String email) throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { return EmailVerificationQueries.getAllEmailVerificationTokenInfoForUser_Transaction(this, sqlCon, - appIdentifier, userId, email); + tenantIdentifier, userId, email); } catch (SQLException e) { throw new StorageQueryException(e); } } @Override - public void deleteAllEmailVerificationTokensForUser_Transaction(AppIdentifier appIdentifier, + public void deleteAllEmailVerificationTokensForUser_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, String userId, String email) throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { - EmailVerificationQueries.deleteAllEmailVerificationTokensForUser_Transaction(this, sqlCon, appIdentifier, userId, email); + EmailVerificationQueries.deleteAllEmailVerificationTokensForUser_Transaction(this, sqlCon, tenantIdentifier, userId, email); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -971,10 +972,10 @@ public void deleteEmailVerificationUserInfo(AppIdentifier appIdentifier, String } @Override - public void addEmailVerificationToken(AppIdentifier appIdentifier, EmailVerificationTokenInfo emailVerificationInfo) + public void addEmailVerificationToken(TenantIdentifier tenantIdentifier, EmailVerificationTokenInfo emailVerificationInfo) throws StorageQueryException, DuplicateEmailVerificationTokenException, TenantOrAppNotFoundException { try { - EmailVerificationQueries.addEmailVerificationToken(this, appIdentifier, emailVerificationInfo.userId, + EmailVerificationQueries.addEmailVerificationToken(this, tenantIdentifier, emailVerificationInfo.userId, emailVerificationInfo.token, emailVerificationInfo.tokenExpiry, emailVerificationInfo.email); } catch (SQLException e) { if (e instanceof PSQLException) { @@ -985,27 +986,27 @@ public void addEmailVerificationToken(AppIdentifier appIdentifier, EmailVerifica throw new DuplicateEmailVerificationTokenException(); } - if (isForeignKeyConstraintError(serverMessage, config.getEmailVerificationTokensTable(), "app_id")) { - throw new TenantOrAppNotFoundException(appIdentifier); + if (isForeignKeyConstraintError(serverMessage, config.getEmailVerificationTokensTable(), "tenant_id")) { + throw new TenantOrAppNotFoundException(tenantIdentifier); } } } } @Override - public EmailVerificationTokenInfo getEmailVerificationTokenInfo(AppIdentifier appIdentifier, String token) + public EmailVerificationTokenInfo getEmailVerificationTokenInfo(TenantIdentifier tenantIdentifier, String token) throws StorageQueryException { try { - return EmailVerificationQueries.getEmailVerificationTokenInfo(this, appIdentifier, token); + return EmailVerificationQueries.getEmailVerificationTokenInfo(this, tenantIdentifier, token); } catch (SQLException e) { throw new StorageQueryException(e); } } @Override - public void revokeAllTokens(AppIdentifier appIdentifier, String userId, String email) throws StorageQueryException { + public void revokeAllTokens(TenantIdentifier tenantIdentifier, String userId, String email) throws StorageQueryException { try { - EmailVerificationQueries.revokeAllTokens(this, appIdentifier, userId, email); + EmailVerificationQueries.revokeAllTokens(this, tenantIdentifier, userId, email); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1021,11 +1022,11 @@ public void unverifyEmail(AppIdentifier appIdentifier, String userId, String ema } @Override - public EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUser(AppIdentifier appIdentifier, + public EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUser(TenantIdentifier tenantIdentifier, String userId, String email) throws StorageQueryException { try { - return EmailVerificationQueries.getAllEmailVerificationTokenInfoForUser(this, appIdentifier, userId, email); + return EmailVerificationQueries.getAllEmailVerificationTokenInfoForUser(this, tenantIdentifier, userId, email); } catch (SQLException e) { throw new StorageQueryException(e); } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/EmailVerificationQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/EmailVerificationQueries.java index aee32bc1..472a5184 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/EmailVerificationQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/EmailVerificationQueries.java @@ -21,6 +21,8 @@ import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.config.Config; import io.supertokens.storage.postgresql.utils.Utils; @@ -61,15 +63,16 @@ static String getQueryToCreateEmailVerificationTokensTable(Start start) { // @formatter:off return "CREATE TABLE IF NOT EXISTS " + emailVerificationTokensTable + " (" + "app_id VARCHAR(64) DEFAULT 'public'," + + "tenant_id VARCHAR(64) DEFAULT 'public'," + "user_id VARCHAR(128) NOT NULL," + "email VARCHAR(256) NOT NULL," + "token VARCHAR(128) NOT NULL CONSTRAINT " + Utils.getConstraintName(schema, emailVerificationTokensTable, "token", "key") + " UNIQUE," + "token_expiry BIGINT NOT NULL," + "CONSTRAINT " + Utils.getConstraintName(schema, emailVerificationTokensTable, null, "pkey") - + " PRIMARY KEY (app_id, user_id, email, token), " - + "CONSTRAINT " + Utils.getConstraintName(schema, emailVerificationTokensTable, "app_id", "fkey") - + " FOREIGN KEY(app_id)" - + " REFERENCES " + Config.getConfig(start).getAppsTable() + " (app_id) ON DELETE CASCADE" + + " PRIMARY KEY (app_id, tenant_id, user_id, email, token), " + + "CONSTRAINT " + Utils.getConstraintName(schema, emailVerificationTokensTable, "tenant_id", "fkey") + + " FOREIGN KEY(app_id, tenant_id)" + + " REFERENCES " + Config.getConfig(start).getTenantsTable() + " (app_id, tenant_id) ON DELETE CASCADE" + ")"; // @formatter:on } @@ -111,26 +114,29 @@ public static void updateUsersIsEmailVerified_Transaction(Start start, Connectio } public static void deleteAllEmailVerificationTokensForUser_Transaction(Start start, Connection con, - AppIdentifier appIdentifier, String userId, + TenantIdentifier tenantIdentifier, String userId, String email) throws SQLException, StorageQueryException { String QUERY = "DELETE FROM " + getConfig(start).getEmailVerificationTokensTable() - + " WHERE app_id = ? AND user_id = ? AND email = ?"; + + " WHERE app_id = ? AND tenant_id = ? AND user_id = ? AND email = ?"; update(con, QUERY, pst -> { - pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, userId); - pst.setString(3, email); + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, userId); + pst.setString(4, email); }); } - public static EmailVerificationTokenInfo getEmailVerificationTokenInfo(Start start, AppIdentifier appIdentifier, + public static EmailVerificationTokenInfo getEmailVerificationTokenInfo(Start start, TenantIdentifier tenantIdentifier, String token) throws SQLException, StorageQueryException { String QUERY = "SELECT user_id, token, token_expiry, email FROM " - + getConfig(start).getEmailVerificationTokensTable() + " WHERE app_id = ? AND token = ?"; + + getConfig(start).getEmailVerificationTokensTable() + + " WHERE app_id = ? AND tenant_id = ? AND token = ?"; return execute(start, QUERY, pst -> { - pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, token); + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, token); }, result -> { if (result.next()) { return EmailVerificationTokenInfoRowMapper.getInstance().mapOrThrow(result); @@ -139,32 +145,34 @@ public static EmailVerificationTokenInfo getEmailVerificationTokenInfo(Start sta }); } - public static void addEmailVerificationToken(Start start, AppIdentifier appIdentifier, String userId, String tokenHash, long expiry, + public static void addEmailVerificationToken(Start start, TenantIdentifier tenantIdentifier, String userId, String tokenHash, long expiry, String email) throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + getConfig(start).getEmailVerificationTokensTable() - + "(app_id, user_id, token, token_expiry, email)" + " VALUES(?, ?, ?, ?, ?)"; + + "(app_id, tenant_id, user_id, token, token_expiry, email)" + " VALUES(?, ?, ?, ?, ?, ?)"; update(start, QUERY, pst -> { - pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, userId); - pst.setString(3, tokenHash); - pst.setLong(4, expiry); - pst.setString(5, email); + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, userId); + pst.setString(4, tokenHash); + pst.setLong(5, expiry); + pst.setString(6, email); }); } public static EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUser_Transaction(Start start, Connection con, - AppIdentifier appIdentifier, + TenantIdentifier tenantIdentifier, String userId, String email) throws SQLException, StorageQueryException { String QUERY = "SELECT user_id, token, token_expiry, email FROM " - + getConfig(start).getEmailVerificationTokensTable() + " WHERE app_id = ? AND user_id = ? AND email = ? FOR UPDATE"; + + getConfig(start).getEmailVerificationTokensTable() + " WHERE app_id = ? AND tenant_id = ? AND user_id = ? AND email = ? FOR UPDATE"; return execute(con, QUERY, pst -> { - pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, userId); - pst.setString(3, email); + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, userId); + pst.setString(4, email); }, result -> { List temp = new ArrayList<>(); while (result.next()) { @@ -179,16 +187,17 @@ public static EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUs } public static EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUser(Start start, - AppIdentifier appIdentifier, + TenantIdentifier tenantIdentifier, String userId, String email) throws SQLException, StorageQueryException { String QUERY = "SELECT user_id, token, token_expiry, email FROM " - + getConfig(start).getEmailVerificationTokensTable() + " WHERE app_id = ? AND user_id = ? AND email = ?"; + + getConfig(start).getEmailVerificationTokensTable() + " WHERE app_id = ? AND tenant_id = ? AND user_id = ? AND email = ?"; return execute(start, QUERY, pst -> { - pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, userId); - pst.setString(3, email); + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, userId); + pst.setString(4, email); }, result -> { List temp = new ArrayList<>(); while (result.next()) { @@ -258,15 +267,16 @@ public static void unverifyEmail(Start start, AppIdentifier appIdentifier, Strin }); } - public static void revokeAllTokens(Start start, AppIdentifier appIdentifier, String userId, String email) + public static void revokeAllTokens(Start start, TenantIdentifier tenantIdentifier, String userId, String email) throws SQLException, StorageQueryException { String QUERY = "DELETE FROM " + getConfig(start).getEmailVerificationTokensTable() - + " WHERE app_id = ? AND user_id = ? AND email = ?"; + + " WHERE app_id = ? AND tenant_id = ? AND user_id = ? AND email = ?"; update(start, QUERY, pst -> { - pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, userId); - pst.setString(3, email); + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, userId); + pst.setString(4, email); }); } diff --git a/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java b/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java index 4de14b77..5db6103e 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java @@ -321,9 +321,9 @@ public void addEmailVerificationTokenExceptions() throws Exception { String userEmail = "useremail@asdf.fdas"; var info = new EmailVerificationTokenInfo(userId, tokenHash, System.currentTimeMillis() + 10000, userEmail); - storage.addEmailVerificationToken(new AppIdentifier(null, null), info); + storage.addEmailVerificationToken(new TenantIdentifier(null, null, null), info); try { - storage.addEmailVerificationToken(new AppIdentifier(null, null), info); + storage.addEmailVerificationToken(new TenantIdentifier(null, null, null), info); throw new Exception("This should throw"); } catch (DuplicateEmailVerificationTokenException ex) { // expected From 154b9b33749dc96205dcbd5b5afa4367cc0ae18e Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Tue, 4 Apr 2023 17:33:38 +0530 Subject: [PATCH 051/106] fix: Multitenant session (#78) * fix: session changes * fix: session changes * fix: session changes --- .../supertokens/storage/postgresql/Start.java | 64 +++--- .../postgresql/queries/SessionQueries.java | 189 ++++++++++++------ .../storage/postgresql/test/ConfigTest.java | 9 +- .../postgresql/test/InMemoryDBTest.java | 13 +- 4 files changed, 174 insertions(+), 101 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 11c1ef65..4f424540 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -357,10 +357,9 @@ public void removeLegacyAccessTokenSigningKey_Transaction(AppIdentifier appIdent public KeyValueInfo[] getAccessTokenSigningKeys_Transaction(AppIdentifier appIdentifier, TransactionConnection con) throws StorageQueryException { - // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { - return SessionQueries.getAccessTokenSigningKeys_Transaction(this, sqlCon); + return SessionQueries.getAccessTokenSigningKeys_Transaction(this, sqlCon, appIdentifier); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -369,12 +368,19 @@ public KeyValueInfo[] getAccessTokenSigningKeys_Transaction(AppIdentifier appIde @Override public void addAccessTokenSigningKey_Transaction(AppIdentifier appIdentifier, TransactionConnection con, KeyValueInfo info) - throws StorageQueryException { - // TODO.. + throws StorageQueryException, TenantOrAppNotFoundException { Connection sqlCon = (Connection) con.getConnection(); try { - SessionQueries.addAccessTokenSigningKey_Transaction(this, sqlCon, info.createdAtTime, info.value); + SessionQueries.addAccessTokenSigningKey_Transaction(this, sqlCon, appIdentifier, info.createdAtTime, info.value); } catch (SQLException e) { + if (e instanceof PSQLException) { + PostgreSQLConfig config = Config.getConfig(this); + ServerErrorMessage serverMessage = ((PSQLException) e).getServerErrorMessage(); + + if (isForeignKeyConstraintError(serverMessage, config.getAccessTokenSigningKeysTable(), "app_id")) { + throw new TenantOrAppNotFoundException(appIdentifier); + } + } throw new StorageQueryException(e); } } @@ -383,8 +389,7 @@ public void addAccessTokenSigningKey_Transaction(AppIdentifier appIdentifier, Tr public void removeAccessTokenSigningKeysBefore(AppIdentifier appIdentifier, long time) throws StorageQueryException { try { - // TODO.. - SessionQueries.removeAccessTokenSigningKeysBefore(this, time); + SessionQueries.removeAccessTokenSigningKeysBefore(this, appIdentifier, time); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -435,22 +440,28 @@ public void createNewSession(TenantIdentifier tenantIdentifier, String sessionHa String refreshTokenHash2, JsonObject userDataInDatabase, long expiry, JsonObject userDataInJWT, long createdAtTime) - throws StorageQueryException { - // TODO.. + throws StorageQueryException, TenantOrAppNotFoundException { try { - SessionQueries.createNewSession(this, sessionHandle, userId, refreshTokenHash2, userDataInDatabase, expiry, - userDataInJWT, createdAtTime); + SessionQueries.createNewSession(this, tenantIdentifier, sessionHandle, userId, refreshTokenHash2, + userDataInDatabase, expiry, userDataInJWT, createdAtTime); } catch (SQLException e) { + if (e instanceof PSQLException) { + PostgreSQLConfig config = Config.getConfig(this); + ServerErrorMessage serverMessage = ((PSQLException) e).getServerErrorMessage(); + + if (isForeignKeyConstraintError(serverMessage, config.getSessionInfoTable(), "tenant_id")) { + throw new TenantOrAppNotFoundException(tenantIdentifier); + } + } throw new StorageQueryException(e); } } @Override - public void deleteSessionsOfUser(AppIdentifier appIdentifierIdentifier, String userId) + public void deleteSessionsOfUser(AppIdentifier appIdentifier, String userId) throws StorageQueryException { try { - // TODO.. - SessionQueries.deleteSessionsOfUser(this, userId); + SessionQueries.deleteSessionsOfUser(this, appIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -459,8 +470,7 @@ public void deleteSessionsOfUser(AppIdentifier appIdentifierIdentifier, String u @Override public int getNumberOfSessions(TenantIdentifier tenantIdentifier) throws StorageQueryException { try { - // TODO.. - return SessionQueries.getNumberOfSessions(this); + return SessionQueries.getNumberOfSessions(this, tenantIdentifier); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -469,8 +479,7 @@ public int getNumberOfSessions(TenantIdentifier tenantIdentifier) throws Storage @Override public int deleteSession(TenantIdentifier tenantIdentifier, String[] sessionHandles) throws StorageQueryException { try { - // TODO.. - return SessionQueries.deleteSession(this, sessionHandles); + return SessionQueries.deleteSession(this, tenantIdentifier, sessionHandles); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -480,8 +489,7 @@ public int deleteSession(TenantIdentifier tenantIdentifier, String[] sessionHand public String[] getAllNonExpiredSessionHandlesForUser(TenantIdentifier tenantIdentifier, String userId) throws StorageQueryException { try { - // TODO.. - return SessionQueries.getAllNonExpiredSessionHandlesForUser(this, userId); + return SessionQueries.getAllNonExpiredSessionHandlesForUser(this, tenantIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -490,8 +498,7 @@ public String[] getAllNonExpiredSessionHandlesForUser(TenantIdentifier tenantIde private String[] getAllNonExpiredSessionHandlesForUser(AppIdentifier appIdentifier, String userId) throws StorageQueryException { try { - // TODO.. - return SessionQueries.getAllNonExpiredSessionHandlesForUser(this, userId); + return SessionQueries.getAllNonExpiredSessionHandlesForUser(this, appIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -536,8 +543,7 @@ public void setStorageLayerEnabled(boolean enabled) { public SessionInfo getSession(TenantIdentifier tenantIdentifier, String sessionHandle) throws StorageQueryException { try { - // TODO.. - return SessionQueries.getSession(this, sessionHandle); + return SessionQueries.getSession(this, tenantIdentifier, sessionHandle); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -548,8 +554,7 @@ public int updateSession(TenantIdentifier tenantIdentifier, String sessionHandle JsonObject jwtPayload) throws StorageQueryException { try { - // TODO.. - return SessionQueries.updateSession(this, sessionHandle, sessionData, jwtPayload); + return SessionQueries.updateSession(this, tenantIdentifier, sessionHandle, sessionData, jwtPayload); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -559,10 +564,9 @@ public int updateSession(TenantIdentifier tenantIdentifier, String sessionHandle public SessionInfo getSessionInfo_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, String sessionHandle) throws StorageQueryException { - // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { - return SessionQueries.getSessionInfo_Transaction(this, sqlCon, sessionHandle); + return SessionQueries.getSessionInfo_Transaction(this, sqlCon, tenantIdentifier, sessionHandle); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -574,8 +578,8 @@ public void updateSessionInfo_Transaction(TenantIdentifier tenantIdentifier, Tra long expiry) throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { - // TODO.. - SessionQueries.updateSessionInfo_Transaction(this, sqlCon, sessionHandle, refreshTokenHash2, expiry); + SessionQueries.updateSessionInfo_Transaction(this, sqlCon, tenantIdentifier, sessionHandle, + refreshTokenHash2, expiry); } catch (SQLException e) { throw new StorageQueryException(e); } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/SessionQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/SessionQueries.java index 087d70cd..d39ad77b 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/SessionQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/SessionQueries.java @@ -21,6 +21,8 @@ import io.supertokens.pluginInterface.KeyValueInfo; import io.supertokens.pluginInterface.RowMapper; import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.session.SessionInfo; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.config.Config; @@ -33,7 +35,6 @@ import java.util.ArrayList; import java.util.List; -import static io.supertokens.storage.postgresql.PreparedStatementValueSetter.NO_OP_SETTER; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.update; import static io.supertokens.storage.postgresql.config.Config.getConfig; @@ -46,6 +47,8 @@ public static String getQueryToCreateSessionInfoTable(Start start) { String sessionInfoTable = Config.getConfig(start).getSessionInfoTable(); // @formatter:off return "CREATE TABLE IF NOT EXISTS " + sessionInfoTable + " (" + + "app_id VARCHAR(64) DEFAULT 'public'," + + "tenant_id VARCHAR(64) DEFAULT 'public'," + "session_handle VARCHAR(255) NOT NULL," + "user_id VARCHAR(128) NOT NULL," + "refresh_token_hash_2 VARCHAR(128) NOT NULL," @@ -53,8 +56,12 @@ public static String getQueryToCreateSessionInfoTable(Start start) { + "expires_at BIGINT NOT NULL," + "created_at_time BIGINT NOT NULL," + "jwt_user_payload TEXT," - + "CONSTRAINT " + Utils.getConstraintName(schema, sessionInfoTable, null, "pkey") + - " PRIMARY KEY(session_handle)" + " );"; + + "CONSTRAINT " + Utils.getConstraintName(schema, sessionInfoTable, null, "pkey") + + " PRIMARY KEY(app_id, tenant_id, session_handle)," + + "CONSTRAINT " + Utils.getConstraintName(schema, sessionInfoTable, "tenant_id", "fkey") + + " FOREIGN KEY (app_id, tenant_id)" + + " REFERENCES " + Config.getConfig(start).getTenantsTable() + "(app_id, tenant_id) ON DELETE CASCADE" + + ");"; // @formatter:on } @@ -64,44 +71,49 @@ static String getQueryToCreateAccessTokenSigningKeysTable(Start start) { String accessTokenSigningKeysTable = Config.getConfig(start).getAccessTokenSigningKeysTable(); // @formatter:off return "CREATE TABLE IF NOT EXISTS " + accessTokenSigningKeysTable + " (" + + "app_id VARCHAR(64) DEFAULT 'public'," + "created_at_time BIGINT NOT NULL," + "value TEXT," - + "CONSTRAINT " + Utils.getConstraintName(schema, accessTokenSigningKeysTable, null, "pkey") + - " PRIMARY KEY(created_at_time)" + " );"; + + "CONSTRAINT " + Utils.getConstraintName(schema, accessTokenSigningKeysTable, null, "pkey") + + " PRIMARY KEY(app_id, created_at_time)," + + "CONSTRAINT " + Utils.getConstraintName(schema, accessTokenSigningKeysTable, "app_id", "fkey") + + " FOREIGN KEY (app_id)" + + " REFERENCES " + Config.getConfig(start).getAppsTable() + "(app_id) ON DELETE CASCADE" + + ");"; // @formatter:on } - public static void createNewSession(Start start, String sessionHandle, String userId, String refreshTokenHash2, - JsonObject userDataInDatabase, long expiry, JsonObject userDataInJWT, long createdAtTime) + public static void createNewSession(Start start, TenantIdentifier tenantIdentifier, String sessionHandle, String userId, String refreshTokenHash2, + JsonObject userDataInDatabase, long expiry, JsonObject userDataInJWT, long createdAtTime) throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + getConfig(start).getSessionInfoTable() - + "(session_handle, user_id, refresh_token_hash_2, session_data, expires_at, jwt_user_payload, " - + "created_at_time)" + " VALUES(?, ?, ?, ?, ?, ?, ?)"; + + "(app_id, tenant_id, session_handle, user_id, refresh_token_hash_2, session_data, expires_at," + + " jwt_user_payload, created_at_time)" + " VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)"; update(start, QUERY, pst -> { - pst.setString(1, sessionHandle); - pst.setString(2, userId); - pst.setString(3, refreshTokenHash2); - pst.setString(4, userDataInDatabase.toString()); - pst.setLong(5, expiry); - pst.setString(6, userDataInJWT.toString()); - pst.setLong(7, createdAtTime); + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, sessionHandle); + pst.setString(4, userId); + pst.setString(5, refreshTokenHash2); + pst.setString(6, userDataInDatabase.toString()); + pst.setLong(7, expiry); + pst.setString(8, userDataInJWT.toString()); + pst.setLong(9, createdAtTime); }); } - static boolean isSessionBlacklisted(Start start, String sessionHandle) throws SQLException, StorageQueryException { - String QUERY = "SELECT session_handle FROM " + getConfig(start).getSessionInfoTable() - + " WHERE session_handle = ?"; - - return execute(start, QUERY, pst -> pst.setString(1, sessionHandle), result -> !result.next()); - } - - public static SessionInfo getSessionInfo_Transaction(Start start, Connection con, String sessionHandle) + public static SessionInfo getSessionInfo_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, + String sessionHandle) throws SQLException, StorageQueryException { String QUERY = "SELECT session_handle, user_id, refresh_token_hash_2, session_data, expires_at, " + "created_at_time, jwt_user_payload FROM " + getConfig(start).getSessionInfoTable() - + " WHERE session_handle = ? FOR UPDATE"; - return execute(con, QUERY, pst -> pst.setString(1, sessionHandle), result -> { + + " WHERE app_id = ? AND tenant_id = ? AND session_handle = ? FOR UPDATE"; + return execute(con, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, sessionHandle); + }, result -> { if (result.next()) { return SessionInfoRowMapper.getInstance().mapOrThrow(result); } @@ -109,22 +121,30 @@ public static SessionInfo getSessionInfo_Transaction(Start start, Connection con }); } - public static void updateSessionInfo_Transaction(Start start, Connection con, String sessionHandle, - String refreshTokenHash2, long expiry) throws SQLException, StorageQueryException { + public static void updateSessionInfo_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, + String sessionHandle, + String refreshTokenHash2, long expiry) throws SQLException, StorageQueryException { String QUERY = "UPDATE " + getConfig(start).getSessionInfoTable() - + " SET refresh_token_hash_2 = ?, expires_at = ?" + " WHERE session_handle = ?"; + + " SET refresh_token_hash_2 = ?, expires_at = ?" + + " WHERE app_id = ? AND tenant_id = ? AND session_handle = ?"; update(con, QUERY, pst -> { pst.setString(1, refreshTokenHash2); pst.setLong(2, expiry); - pst.setString(3, sessionHandle); + pst.setString(3, tenantIdentifier.getAppId()); + pst.setString(4, tenantIdentifier.getTenantId()); + pst.setString(5, sessionHandle); }); } - public static int getNumberOfSessions(Start start) throws SQLException, StorageQueryException { - String QUERY = "SELECT count(*) as num FROM " + getConfig(start).getSessionInfoTable(); + public static int getNumberOfSessions(Start start, TenantIdentifier tenantIdentifier) throws SQLException, StorageQueryException { + String QUERY = "SELECT count(*) as num FROM " + getConfig(start).getSessionInfoTable() + + " WHERE app_id = ? AND tenant_id = ?"; - return execute(start, QUERY, NO_OP_SETTER, result -> { + return execute(start, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + }, result -> { if (result.next()) { return result.getInt("num"); } @@ -132,12 +152,13 @@ public static int getNumberOfSessions(Start start) throws SQLException, StorageQ }); } - public static int deleteSession(Start start, String[] sessionHandles) throws SQLException, StorageQueryException { + public static int deleteSession(Start start, TenantIdentifier tenantIdentifier, String[] sessionHandles) throws SQLException, StorageQueryException { if (sessionHandles.length == 0) { return 0; } StringBuilder QUERY = new StringBuilder( - "DELETE FROM " + Config.getConfig(start).getSessionInfoTable() + " WHERE session_handle IN ("); + "DELETE FROM " + Config.getConfig(start).getSessionInfoTable() + + " WHERE app_id = ? AND tenant_id = ? AND session_handle IN ("); for (int i = 0; i < sessionHandles.length; i++) { if (i == sessionHandles.length - 1) { QUERY.append("?)"); @@ -147,26 +168,56 @@ public static int deleteSession(Start start, String[] sessionHandles) throws SQL } return update(start, QUERY.toString(), pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); for (int i = 0; i < sessionHandles.length; i++) { - pst.setString(i + 1, sessionHandles[i]); + pst.setString(i + 3, sessionHandles[i]); } }); } - public static void deleteSessionsOfUser(Start start, String userId) throws SQLException, StorageQueryException { - String QUERY = "DELETE FROM " + getConfig(start).getSessionInfoTable() + " WHERE user_id = ?"; + public static void deleteSessionsOfUser(Start start, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { + String QUERY = "DELETE FROM " + getConfig(start).getSessionInfoTable() + + " WHERE app_id = ? AND user_id = ?"; - update(start, QUERY.toString(), pst -> pst.setString(1, userId)); + update(start, QUERY.toString(), pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); } - public static String[] getAllNonExpiredSessionHandlesForUser(Start start, String userId) + public static String[] getAllNonExpiredSessionHandlesForUser(Start start, TenantIdentifier tenantIdentifier, String userId) throws SQLException, StorageQueryException { String QUERY = "SELECT session_handle FROM " + getConfig(start).getSessionInfoTable() - + " WHERE user_id = ? AND expires_at >= ?"; + + " WHERE app_id = ? AND tenant_id = ? AND user_id = ? AND expires_at >= ?"; return execute(start, QUERY, pst -> { - pst.setString(1, userId); - pst.setLong(2, currentTimeMillis()); + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, userId); + pst.setLong(4, currentTimeMillis()); + }, result -> { + List temp = new ArrayList<>(); + while (result.next()) { + temp.add(result.getString("session_handle")); + } + String[] finalResult = new String[temp.size()]; + for (int i = 0; i < temp.size(); i++) { + finalResult[i] = temp.get(i); + } + return finalResult; + }); + } + + public static String[] getAllNonExpiredSessionHandlesForUser(Start start, AppIdentifier appIdentifier, String userId) + throws SQLException, StorageQueryException { + String QUERY = "SELECT session_handle FROM " + getConfig(start).getSessionInfoTable() + + " WHERE app_id = ? AND user_id = ? AND expires_at >= ?"; + + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + pst.setLong(3, currentTimeMillis()); }, result -> { List temp = new ArrayList<>(); while (result.next()) { @@ -186,8 +237,8 @@ public static void deleteAllExpiredSessions(Start start) throws SQLException, St update(start, QUERY, pst -> pst.setLong(1, currentTimeMillis())); } - public static int updateSession(Start start, String sessionHandle, @Nullable JsonObject sessionData, - @Nullable JsonObject jwtPayload) throws SQLException, StorageQueryException { + public static int updateSession(Start start, TenantIdentifier tenantIdentifier, String sessionHandle, @Nullable JsonObject sessionData, + @Nullable JsonObject jwtPayload) throws SQLException, StorageQueryException { if (sessionData == null && jwtPayload == null) { throw new SQLException("sessionData and jwtPayload are null when updating session info"); @@ -202,7 +253,7 @@ public static int updateSession(Start start, String sessionHandle, @Nullable Jso if (jwtPayload != null) { QUERY += (somethingBefore ? "," : "") + " jwt_user_payload = ?"; } - QUERY += " WHERE session_handle = ?"; + QUERY += " WHERE app_id = ? AND tenant_id = ? AND session_handle = ?"; return update(start, QUERY, pst -> { int currIndex = 1; @@ -214,15 +265,21 @@ public static int updateSession(Start start, String sessionHandle, @Nullable Jso pst.setString(currIndex, jwtPayload.toString()); currIndex++; } + pst.setString(currIndex++, tenantIdentifier.getAppId()); + pst.setString(currIndex++, tenantIdentifier.getTenantId()); pst.setString(currIndex, sessionHandle); }); } - public static SessionInfo getSession(Start start, String sessionHandle) throws SQLException, StorageQueryException { + public static SessionInfo getSession(Start start, TenantIdentifier tenantIdentifier, String sessionHandle) throws SQLException, StorageQueryException { String QUERY = "SELECT session_handle, user_id, refresh_token_hash_2, session_data, expires_at, " + "created_at_time, jwt_user_payload FROM " + getConfig(start).getSessionInfoTable() - + " WHERE session_handle = ?"; - return execute(start, QUERY, pst -> pst.setString(1, sessionHandle), result -> { + + " WHERE app_id = ? AND tenant_id = ? AND session_handle = ?"; + return execute(start, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, sessionHandle); + }, result -> { if (result.next()) { return SessionInfoRowMapper.getInstance().mapOrThrow(result); } @@ -230,22 +287,29 @@ public static SessionInfo getSession(Start start, String sessionHandle) throws S }); } - public static void addAccessTokenSigningKey_Transaction(Start start, Connection con, long createdAtTime, - String value) throws SQLException, StorageQueryException { - String QUERY = "INSERT INTO " + getConfig(start).getAccessTokenSigningKeysTable() + "(created_at_time, value)" - + " VALUES(?, ?)"; + public static void addAccessTokenSigningKey_Transaction(Start start, Connection con, AppIdentifier appIdentifier, + long createdAtTime, + String value) throws SQLException, StorageQueryException { + String QUERY = "INSERT INTO " + getConfig(start).getAccessTokenSigningKeysTable() + + "(app_id, created_at_time, value)" + + " VALUES(?, ?, ?)"; update(con, QUERY, pst -> { - pst.setLong(1, createdAtTime); - pst.setString(2, value); + pst.setString(1, appIdentifier.getAppId()); + pst.setLong(2, createdAtTime); + pst.setString(3, value); }); } - public static KeyValueInfo[] getAccessTokenSigningKeys_Transaction(Start start, Connection con) + public static KeyValueInfo[] getAccessTokenSigningKeys_Transaction(Start start, Connection con, + AppIdentifier appIdentifier) throws SQLException, StorageQueryException { - String QUERY = "SELECT * FROM " + getConfig(start).getAccessTokenSigningKeysTable() + " FOR UPDATE"; + String QUERY = "SELECT * FROM " + getConfig(start).getAccessTokenSigningKeysTable() + + " WHERE app_id = ? FOR UPDATE"; - return execute(con, QUERY, NO_OP_SETTER, result -> { + return execute(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + }, result -> { List temp = new ArrayList<>(); while (result.next()) { temp.add(AccessTokenSigningKeyRowMapper.getInstance().mapOrThrow(result)); @@ -258,12 +322,15 @@ public static KeyValueInfo[] getAccessTokenSigningKeys_Transaction(Start start, }); } - public static void removeAccessTokenSigningKeysBefore(Start start, long time) + public static void removeAccessTokenSigningKeysBefore(Start start, AppIdentifier appIdentifier, long time) throws SQLException, StorageQueryException { String QUERY = "DELETE FROM " + getConfig(start).getAccessTokenSigningKeysTable() - + " WHERE created_at_time < ?"; + + " WHERE app_id = ? AND created_at_time < ?"; - update(start, QUERY, pst -> pst.setLong(1, time)); + update(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setLong(2, time); + }); } static class SessionInfoRowMapper implements RowMapper { diff --git a/src/test/java/io/supertokens/storage/postgresql/test/ConfigTest.java b/src/test/java/io/supertokens/storage/postgresql/test/ConfigTest.java index 7ba054c0..b6971754 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/ConfigTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/ConfigTest.java @@ -22,6 +22,7 @@ import com.google.gson.JsonObject; import io.supertokens.ProcessState; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.session.SessionStorage; import io.supertokens.session.Session; import io.supertokens.session.info.SessionInformationHolder; import io.supertokens.storage.postgresql.ConnectionPoolTestContent; @@ -311,7 +312,7 @@ public void testAddingSchemaWorks() throws Exception { assert sessionInfo.accessToken != null; assert sessionInfo.refreshToken != null; - TestCase.assertEquals(StorageLayer.getSessionStorage(process.getProcess()) + TestCase.assertEquals(((SessionStorage) StorageLayer.getStorage(process.getProcess())) .getNumberOfSessions(new TenantIdentifier(null, null, null)), 1); process.kill(); @@ -355,7 +356,7 @@ public void testAddingSchemaViaConnectionUriWorks() throws Exception { assert sessionInfo.accessToken != null; assert sessionInfo.refreshToken != null; - TestCase.assertEquals(StorageLayer.getSessionStorage(process.getProcess()) + TestCase.assertEquals(((SessionStorage) StorageLayer.getStorage(process.getProcess())) .getNumberOfSessions(new TenantIdentifier(null, null, null)), 1); process.kill(); @@ -399,7 +400,7 @@ public void testAddingSchemaViaConnectionUriWorks2() throws Exception { assert sessionInfo.accessToken != null; assert sessionInfo.refreshToken != null; - TestCase.assertEquals(StorageLayer.getSessionStorage(process.getProcess()) + TestCase.assertEquals(((SessionStorage) StorageLayer.getStorage(process.getProcess())) .getNumberOfSessions(new TenantIdentifier(null, null, null)), 1); process.kill(); @@ -443,7 +444,7 @@ public void testAddingSchemaViaConnectionUriWorks3() throws Exception { assert sessionInfo.accessToken != null; assert sessionInfo.refreshToken != null; - TestCase.assertEquals(StorageLayer.getSessionStorage(process.getProcess()) + TestCase.assertEquals(((SessionStorage) StorageLayer.getStorage(process.getProcess())) .getNumberOfSessions(new TenantIdentifier(null, null, null)), 1); process.kill(); diff --git a/src/test/java/io/supertokens/storage/postgresql/test/InMemoryDBTest.java b/src/test/java/io/supertokens/storage/postgresql/test/InMemoryDBTest.java index 92727b4e..14697ea4 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/InMemoryDBTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/InMemoryDBTest.java @@ -22,6 +22,7 @@ import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.session.SessionStorage; import io.supertokens.session.Session; import io.supertokens.session.info.SessionInformationHolder; import io.supertokens.storageLayer.StorageLayer; @@ -85,7 +86,7 @@ public void checkThatInMemDVWorksEvenIfWrongConfig() assert sessionInfo.accessToken != null; assert sessionInfo.refreshToken != null; - assertEquals(StorageLayer.getSessionStorage(process.getProcess()) + assertEquals(((SessionStorage) StorageLayer.getStorage(process.getProcess())) .getNumberOfSessions(new TenantIdentifier(null, null, null)), 1); process.kill(); @@ -98,7 +99,7 @@ public void checkThatInMemDVWorksEvenIfWrongConfig() TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - assertEquals(StorageLayer.getSessionStorage(process.getProcess()) + assertEquals(((SessionStorage) StorageLayer.getStorage(process.getProcess())) .getNumberOfSessions(new TenantIdentifier(null, null, null)), 0); process.kill(); @@ -128,7 +129,7 @@ public void checkThatActualDBWorksIfCorrectConfigDev() throws InterruptedExcepti assert sessionInfo.accessToken != null; assert sessionInfo.refreshToken != null; - assertEquals(StorageLayer.getSessionStorage(process.getProcess()) + assertEquals(((SessionStorage) StorageLayer.getStorage(process.getProcess())) .getNumberOfSessions(new TenantIdentifier(null, null, null)), 1); process.kill(); @@ -139,7 +140,7 @@ public void checkThatActualDBWorksIfCorrectConfigDev() throws InterruptedExcepti TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - assertEquals(StorageLayer.getSessionStorage(process.getProcess()) + assertEquals(((SessionStorage) StorageLayer.getStorage(process.getProcess())) .getNumberOfSessions(new TenantIdentifier(null, null, null)), 1); process.kill(); @@ -169,7 +170,7 @@ public void checkThatActualDBWorksIfCorrectConfigProduction() throws Interrupted assert sessionInfo.accessToken != null; assert sessionInfo.refreshToken != null; - assertEquals(StorageLayer.getSessionStorage(process.getProcess()) + assertEquals(((SessionStorage) StorageLayer.getStorage(process.getProcess())) .getNumberOfSessions(new TenantIdentifier(null, null, null)), 1); process.kill(); @@ -180,7 +181,7 @@ public void checkThatActualDBWorksIfCorrectConfigProduction() throws Interrupted TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - assertEquals(StorageLayer.getSessionStorage(process.getProcess()) + assertEquals(((SessionStorage) StorageLayer.getStorage(process.getProcess())) .getNumberOfSessions(new TenantIdentifier(null, null, null)), 1); process.kill(); From 7800c549492ed3310d4c21dd9db9be62da4d6e52 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Wed, 5 Apr 2023 12:43:02 +0530 Subject: [PATCH 052/106] comment modification --- .../java/io/supertokens/storage/postgresql/config/Config.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/config/Config.java b/src/main/java/io/supertokens/storage/postgresql/config/Config.java index ac8bbdd8..c41d93ef 100644 --- a/src/main/java/io/supertokens/storage/postgresql/config/Config.java +++ b/src/main/java/io/supertokens/storage/postgresql/config/Config.java @@ -61,7 +61,6 @@ public static void loadConfig(Start start, JsonObject configJson, Set } public static String getUserPoolId(Start start) { - // this function returns a unique string per connection pool. // TODO: The way things are implemented right now, this function has the issue that if the user points to the // same database, but with a different host (cause the db is reachable via two hosts as an example), // then it will return two different user pool IDs - which is technically the wrong thing to do. From aaa94c2688cbb31a4d609eb74253ef7b220a9621 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Wed, 5 Apr 2023 17:15:35 +0530 Subject: [PATCH 053/106] fix: Multitenant session changes (#80) * fix: key value changes * fix: pr comments * fix: adding tenant or app not found exceptions --- .../supertokens/storage/postgresql/Start.java | 50 ++++++++++------ .../postgresql/queries/GeneralQueries.java | 60 +++++++++++++------ .../storage/postgresql/test/DeadlockTest.java | 13 ++-- 3 files changed, 81 insertions(+), 42 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 4f424540..18be8a95 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -332,10 +332,10 @@ public void commitTransaction(TransactionConnection con) throws StorageQueryExce public KeyValueInfo getLegacyAccessTokenSigningKey_Transaction(AppIdentifier appIdentifier, TransactionConnection con) throws StorageQueryException { - // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { - return GeneralQueries.getKeyValue_Transaction(this, sqlCon, ACCESS_TOKEN_SIGNING_KEY_NAME); + return GeneralQueries.getKeyValue_Transaction(this, sqlCon, + appIdentifier.getAsPublicTenantIdentifier(), ACCESS_TOKEN_SIGNING_KEY_NAME); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -344,10 +344,10 @@ public KeyValueInfo getLegacyAccessTokenSigningKey_Transaction(AppIdentifier app @Override public void removeLegacyAccessTokenSigningKey_Transaction(AppIdentifier appIdentifier, TransactionConnection con) throws StorageQueryException { - // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { - GeneralQueries.deleteKeyValue_Transaction(this, sqlCon, ACCESS_TOKEN_SIGNING_KEY_NAME); + GeneralQueries.deleteKeyValue_Transaction(this, sqlCon, + appIdentifier.getAsPublicTenantIdentifier(), ACCESS_TOKEN_SIGNING_KEY_NAME); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -398,10 +398,10 @@ public void removeAccessTokenSigningKeysBefore(AppIdentifier appIdentifier, long @Override public KeyValueInfo getRefreshTokenSigningKey_Transaction(AppIdentifier appIdentifier, TransactionConnection con) throws StorageQueryException { - // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { - return GeneralQueries.getKeyValue_Transaction(this, sqlCon, REFRESH_TOKEN_KEY_NAME); + return GeneralQueries.getKeyValue_Transaction(this, sqlCon, + appIdentifier.getAsPublicTenantIdentifier(), REFRESH_TOKEN_KEY_NAME); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -410,11 +410,11 @@ public KeyValueInfo getRefreshTokenSigningKey_Transaction(AppIdentifier appIdent @Override public void setRefreshTokenSigningKey_Transaction(AppIdentifier appIdentifier, TransactionConnection con, KeyValueInfo info) - throws StorageQueryException { - // TODO.. + throws StorageQueryException, TenantOrAppNotFoundException { Connection sqlCon = (Connection) con.getConnection(); try { - GeneralQueries.setKeyValue_Transaction(this, sqlCon, REFRESH_TOKEN_KEY_NAME, info); + GeneralQueries.setKeyValue_Transaction(this, sqlCon, + appIdentifier.getAsPublicTenantIdentifier(), REFRESH_TOKEN_KEY_NAME, info); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -515,9 +515,8 @@ public void deleteAllExpiredSessions() throws StorageQueryException { @Override public KeyValueInfo getKeyValue(TenantIdentifier tenantIdentifier, String key) throws StorageQueryException { - // TODO.. try { - return GeneralQueries.getKeyValue(this, key); + return GeneralQueries.getKeyValue(this, tenantIdentifier, key); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -525,11 +524,18 @@ public KeyValueInfo getKeyValue(TenantIdentifier tenantIdentifier, String key) t @Override public void setKeyValue(TenantIdentifier tenantIdentifier, String key, KeyValueInfo info) - throws StorageQueryException { - // TODO.. + throws StorageQueryException, TenantOrAppNotFoundException { try { - GeneralQueries.setKeyValue(this, key, info); + GeneralQueries.setKeyValue(this, tenantIdentifier, key, info); } catch (SQLException e) { + if (e instanceof PSQLException) { + PostgreSQLConfig config = Config.getConfig(this); + ServerErrorMessage serverMessage = ((PSQLException) e).getServerErrorMessage(); + + if (isForeignKeyConstraintError(serverMessage, config.getKeyValueTable(), "tenant_id")) { + throw new TenantOrAppNotFoundException(tenantIdentifier); + } + } throw new StorageQueryException(e); } } @@ -588,12 +594,19 @@ public void updateSessionInfo_Transaction(TenantIdentifier tenantIdentifier, Tra @Override public void setKeyValue_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, String key, KeyValueInfo info) - throws StorageQueryException { - // TODO.. + throws StorageQueryException, TenantOrAppNotFoundException { Connection sqlCon = (Connection) con.getConnection(); try { - GeneralQueries.setKeyValue_Transaction(this, sqlCon, key, info); + GeneralQueries.setKeyValue_Transaction(this, sqlCon, tenantIdentifier, key, info); } catch (SQLException e) { + if (e instanceof PSQLException) { + PostgreSQLConfig config = Config.getConfig(this); + ServerErrorMessage serverMessage = ((PSQLException) e).getServerErrorMessage(); + + if (isForeignKeyConstraintError(serverMessage, config.getKeyValueTable(), "tenant_id")) { + throw new TenantOrAppNotFoundException(tenantIdentifier); + } + } throw new StorageQueryException(e); } } @@ -601,10 +614,9 @@ public void setKeyValue_Transaction(TenantIdentifier tenantIdentifier, Transacti @Override public KeyValueInfo getKeyValue_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, String key) throws StorageQueryException { - // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { - return GeneralQueries.getKeyValue_Transaction(this, sqlCon, key); + return GeneralQueries.getKeyValue_Transaction(this, sqlCon, tenantIdentifier, key); } catch (SQLException e) { throw new StorageQueryException(e); } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java index c444feee..b2ed6aab 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java @@ -128,11 +128,17 @@ private static String getQueryToCreateKeyValueTable(Start start) { String keyValueTable = Config.getConfig(start).getKeyValueTable(); // @formatter:off return "CREATE TABLE IF NOT EXISTS " + keyValueTable + " (" + + "app_id VARCHAR(64) DEFAULT 'public'," + + "tenant_id VARCHAR(64) DEFAULT 'public'," + "name VARCHAR(128)," + "value TEXT," + "created_at_time BIGINT ," - + "CONSTRAINT " + Utils.getConstraintName(schema, keyValueTable, null, "pkey") + " PRIMARY KEY(name)" + - " );"; + + "CONSTRAINT " + Utils.getConstraintName(schema, keyValueTable, null, "pkey") + + " PRIMARY KEY(app_id, tenant_id, name)," + + "CONSTRAINT " + Utils.getConstraintName(schema, keyValueTable, "tenant_id", "fkey") + + " FOREIGN KEY(app_id, tenant_id)" + + " REFERENCES " + Config.getConfig(start).getTenantsTable() + " (app_id, tenant_id) ON DELETE CASCADE" + + ");"; // @formatter:on } @@ -402,32 +408,39 @@ public static void deleteAllTables(Start start) throws SQLException, StorageQuer } } - public static void setKeyValue_Transaction(Start start, Connection con, String key, KeyValueInfo info) + public static void setKeyValue_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, String key, KeyValueInfo info) throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + getConfig(start).getKeyValueTable() - + "(name, value, created_at_time) VALUES(?, ?, ?) " - + "ON CONFLICT (name) DO UPDATE SET value = ?, created_at_time = ?"; + + "(app_id, tenant_id, name, value, created_at_time) VALUES(?, ?, ?, ?, ?) " + + "ON CONFLICT (app_id, tenant_id, name) DO UPDATE SET value = ?, created_at_time = ?"; update(con, QUERY, pst -> { - pst.setString(1, key); - pst.setString(2, info.value); - pst.setLong(3, info.createdAtTime); + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, key); pst.setString(4, info.value); pst.setLong(5, info.createdAtTime); + pst.setString(6, info.value); + pst.setLong(7, info.createdAtTime); }); } - public static void setKeyValue(Start start, String key, KeyValueInfo info) + public static void setKeyValue(Start start, TenantIdentifier tenantIdentifier, String key, KeyValueInfo info) throws SQLException, StorageQueryException { try (Connection con = ConnectionPool.getConnection(start)) { - setKeyValue_Transaction(start, con, key, info); + setKeyValue_Transaction(start, con, tenantIdentifier, key, info); } } - public static KeyValueInfo getKeyValue(Start start, String key) throws SQLException, StorageQueryException { - String QUERY = "SELECT value, created_at_time FROM " + getConfig(start).getKeyValueTable() + " WHERE name = ?"; + public static KeyValueInfo getKeyValue(Start start, TenantIdentifier tenantIdentifier, String key) throws SQLException, StorageQueryException { + String QUERY = "SELECT value, created_at_time FROM " + getConfig(start).getKeyValueTable() + + " WHERE app_id = ? AND tenant_id = ? AND name = ?"; - return execute(start, QUERY, pst -> pst.setString(1, key), result -> { + return execute(start, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, key); + }, result -> { if (result.next()) { return KeyValueInfoRowMapper.getInstance().mapOrThrow(result); } @@ -435,12 +448,16 @@ public static KeyValueInfo getKeyValue(Start start, String key) throws SQLExcept }); } - public static KeyValueInfo getKeyValue_Transaction(Start start, Connection con, String key) + public static KeyValueInfo getKeyValue_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, String key) throws SQLException, StorageQueryException { String QUERY = "SELECT value, created_at_time FROM " + getConfig(start).getKeyValueTable() - + " WHERE name = ? FOR UPDATE"; + + " WHERE app_id = ? AND tenant_id = ? AND name = ? FOR UPDATE"; - return execute(con, QUERY, pst -> pst.setString(1, key), result -> { + return execute(con, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, key); + }, result -> { if (result.next()) { return KeyValueInfoRowMapper.getInstance().mapOrThrow(result); } @@ -448,11 +465,16 @@ public static KeyValueInfo getKeyValue_Transaction(Start start, Connection con, }); } - public static void deleteKeyValue_Transaction(Start start, Connection con, String key) + public static void deleteKeyValue_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, String key) throws SQLException, StorageQueryException { - String QUERY = "DELETE FROM " + getConfig(start).getKeyValueTable() + " WHERE name = ?"; + String QUERY = "DELETE FROM " + getConfig(start).getKeyValueTable() + + " WHERE app_id = ? AND tenant_id = ? AND name = ?"; - update(con, QUERY, pst -> pst.setString(1, key)); + update(con, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, key); + }); } public static long getUsersCount(Start start, RECIPE_ID[] includeRecipeIds) diff --git a/src/test/java/io/supertokens/storage/postgresql/test/DeadlockTest.java b/src/test/java/io/supertokens/storage/postgresql/test/DeadlockTest.java index 03c4bab3..71a4b17f 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/DeadlockTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/DeadlockTest.java @@ -24,6 +24,7 @@ import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.sqlStorage.SQLStorage; import io.supertokens.storageLayer.StorageLayer; import org.junit.AfterClass; @@ -64,10 +65,14 @@ public void transactionDeadlockTesting() Storage storage = StorageLayer.getStorage(process.getProcess()); SQLStorage sqlStorage = (SQLStorage) storage; sqlStorage.startTransaction(con -> { - sqlStorage.setKeyValue_Transaction(new TenantIdentifier(null, null, null), con, "Key", - new KeyValueInfo("Value")); - sqlStorage.setKeyValue_Transaction(new TenantIdentifier(null, null, null), con, "Key1", - new KeyValueInfo("Value1")); + try { + sqlStorage.setKeyValue_Transaction(new TenantIdentifier(null, null, null), con, "Key", + new KeyValueInfo("Value")); + sqlStorage.setKeyValue_Transaction(new TenantIdentifier(null, null, null), con, "Key1", + new KeyValueInfo("Value1")); + } catch (TenantOrAppNotFoundException e) { + throw new IllegalStateException(e); + } sqlStorage.commitTransaction(con); return null; }); From 8e71b3e0816542da68b71074e54697b17aa2bcdd Mon Sep 17 00:00:00 2001 From: Rishabh Poddar Date: Wed, 5 Apr 2023 18:18:03 +0530 Subject: [PATCH 054/106] Multi tenant merging with latest (#79) * merges with latest * fixes test compilation issue * increases threshold of deadlock retries * adds simple test for loading 50 storages --- CHANGELOG.md | 17 +- build.gradle | 2 +- ...-2.2.0.jar => postgresql-plugin-2.4.0.jar} | Bin 121683 -> 134485 bytes pluginInterfaceSupported.json | 4 +- .../storage/postgresql/ProcessState.java | 4 + .../supertokens/storage/postgresql/Start.java | 450 ++++++++++++++---- .../postgresql/config/PostgreSQLConfig.java | 21 +- .../queries/ActiveUsersQueries.java | 68 +++ .../postgresql/queries/GeneralQueries.java | 214 ++++++++- .../postgresql/queries/TOTPQueries.java | 255 ++++++++++ .../storage/postgresql/test/DeadlockTest.java | 406 +++++++++++++++- .../storage/postgresql/test/Retry.java | 56 +++ .../postgresql/test/StorageLayerTest.java | 93 ++++ .../test/multitenancy/StorageLayerTest.java | 63 ++- startDb.sh | 2 +- 15 files changed, 1528 insertions(+), 127 deletions(-) rename jar/{postgresql-plugin-2.2.0.jar => postgresql-plugin-2.4.0.jar} (59%) create mode 100644 src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java create mode 100644 src/main/java/io/supertokens/storage/postgresql/queries/TOTPQueries.java create mode 100644 src/test/java/io/supertokens/storage/postgresql/test/Retry.java create mode 100644 src/test/java/io/supertokens/storage/postgresql/test/StorageLayerTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index f79ad4b5..ba3ea291 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,21 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [2.4.0] - 2023-03-30 + +- Support for Dashboard Search + +## [2.3.0] - 2023-03-27 +- Support for TOTP recipe +- Support for active users + +### Database changes +- Add new tables for TOTP recipe: + - `totp_users` that stores the users that have enabled TOTP + - `totp_user_devices` that stores devices (each device has its own secret) for each user + - `totp_used_codes` that stores used codes for each user. This is to implement rate limiting and prevent replay attacks. +- Add `user_last_active` table to store the last active time of a user. + ## [2.2.0] - 2023-02-21 - Adds support for Dashboard recipe @@ -156,4 +171,4 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixed -- The core now waits for the PostgrSQL db to start \ No newline at end of file +- The core now waits for the PostgrSQL db to start diff --git a/build.gradle b/build.gradle index a3108d1b..af99adcb 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'java-library' } -version = "2.2.0" +version = "2.4.0" repositories { mavenCentral() diff --git a/jar/postgresql-plugin-2.2.0.jar b/jar/postgresql-plugin-2.4.0.jar similarity index 59% rename from jar/postgresql-plugin-2.2.0.jar rename to jar/postgresql-plugin-2.4.0.jar index b591aaf750d9dddb0da0939c848b5820b8397293..21a79212ae38c2cdabc35c0dbafda49215331325 100644 GIT binary patch delta 49458 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

|RVZQw8G3fbQZoNYB7E~9lgtHf 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 HaKQXObA>z2 delta 37018 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 T startTransaction(TransactionLogic logic, TransactionIsolationLev // we have deadlock as well due to the DeadlockTest.java exceptionMessage.toLowerCase().contains("deadlock"); - - if ((isPSQLRollbackException || isDeadlockException) && tries < 3) { + if ((isPSQLRollbackException || isDeadlockException) && tries < 50) { try { Thread.sleep((long) (10 + (Math.random() * 20))); } catch (InterruptedException ignored) { @@ -423,6 +431,7 @@ public void setRefreshTokenSigningKey_Transaction(AppIdentifier appIdentifier, T @TestOnly @Override public void deleteAllInformation() throws StorageQueryException { + ProcessState.getInstance(this).clear(); try { GeneralQueries.deleteAllTables(this); } catch (SQLException e) { @@ -667,6 +676,13 @@ public boolean isUserIdBeingUsedInNonAuthRecipe(AppIdentifier appIdentifier, Str } catch (SQLException e) { throw new StorageQueryException(e); } + } else if (className.equals(TOTPStorage.class.getName())) { + try { + TOTPDevice[] devices = TOTPQueries.getDevices(this, userId); + return devices.length > 0; + } catch (SQLException e) { + throw new StorageQueryException(e); + } } else if (className.equals(JWTRecipeStorage.class.getName())) { return false; } else { @@ -734,6 +750,13 @@ public void addInfoToNonAuthRecipesBasedOnUserId(String className, String userId } throw new StorageQueryException(e); } + } else if (className.equals(TOTPStorage.class.getName())) { + try { + TOTPDevice device = new TOTPDevice(userId, "testDevice", "secret", 0, 30, false); + TOTPQueries.createDevice(this, device); + } catch (StorageTransactionLogicException e) { + throw new StorageQueryException(e.actualException); + } } else if (className.equals(JWTRecipeStorage.class.getName())) { /* Since JWT recipe tables do not store userId we do not add any data to them */ } else { @@ -751,7 +774,8 @@ public void signUp(TenantIdentifier tenantIdentifier, UserInfo userInfo) throws StorageQueryException, DuplicateUserIdException, DuplicateEmailException, TenantOrAppNotFoundException { try { - EmailPasswordQueries.signUp(this, tenantIdentifier, userInfo.id, userInfo.email, userInfo.passwordHash, userInfo.timeJoined); + EmailPasswordQueries.signUp(this, tenantIdentifier, userInfo.id, userInfo.email, userInfo.passwordHash, + userInfo.timeJoined); } catch (StorageTransactionLogicException eTemp) { Exception e = eTemp.actualException; if (e instanceof PSQLException) { @@ -853,7 +877,8 @@ public PasswordResetTokenInfo[] getAllPasswordResetTokenInfoForUser_Transaction( throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { - return EmailPasswordQueries.getAllPasswordResetTokenInfoForUser_Transaction(this, sqlCon, appIdentifier, userId); + return EmailPasswordQueries.getAllPasswordResetTokenInfoForUser_Transaction(this, sqlCon, appIdentifier, + userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -942,7 +967,8 @@ public void deleteAllEmailVerificationTokensForUser_Transaction(TenantIdentifier String email) throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { - EmailVerificationQueries.deleteAllEmailVerificationTokensForUser_Transaction(this, sqlCon, tenantIdentifier, userId, email); + EmailVerificationQueries.deleteAllEmailVerificationTokensForUser_Transaction(this, sqlCon, tenantIdentifier, + userId, email); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -955,7 +981,8 @@ public void updateIsEmailVerified_Transaction(AppIdentifier appIdentifier, Trans throws StorageQueryException, TenantOrAppNotFoundException { Connection sqlCon = (Connection) con.getConnection(); try { - EmailVerificationQueries.updateUsersIsEmailVerified_Transaction(this, sqlCon, appIdentifier, userId, email, + EmailVerificationQueries.updateUsersIsEmailVerified_Transaction(this, sqlCon, appIdentifier, userId, + email, isEmailVerified); } catch (SQLException e) { if (e instanceof PSQLException) { @@ -968,7 +995,8 @@ public void updateIsEmailVerified_Transaction(AppIdentifier appIdentifier, Trans } boolean isPSQLPrimKeyError = e instanceof PSQLException && isPrimaryKeyError( - ((PSQLException) e).getServerErrorMessage(), Config.getConfig(this).getEmailVerificationTable()); + ((PSQLException) e).getServerErrorMessage(), + Config.getConfig(this).getEmailVerificationTable()); if (!isEmailVerified || !isPSQLPrimKeyError) { throw new StorageQueryException(e); @@ -988,7 +1016,8 @@ public void deleteEmailVerificationUserInfo(AppIdentifier appIdentifier, String } @Override - public void addEmailVerificationToken(TenantIdentifier tenantIdentifier, EmailVerificationTokenInfo emailVerificationInfo) + public void addEmailVerificationToken(TenantIdentifier tenantIdentifier, + EmailVerificationTokenInfo emailVerificationInfo) throws StorageQueryException, DuplicateEmailVerificationTokenException, TenantOrAppNotFoundException { try { EmailVerificationQueries.addEmailVerificationToken(this, tenantIdentifier, emailVerificationInfo.userId, @@ -1002,7 +1031,8 @@ public void addEmailVerificationToken(TenantIdentifier tenantIdentifier, EmailVe throw new DuplicateEmailVerificationTokenException(); } - if (isForeignKeyConstraintError(serverMessage, config.getEmailVerificationTokensTable(), "tenant_id")) { + if (isForeignKeyConstraintError(serverMessage, config.getEmailVerificationTokensTable(), + "tenant_id")) { throw new TenantOrAppNotFoundException(tenantIdentifier); } } @@ -1020,7 +1050,8 @@ public EmailVerificationTokenInfo getEmailVerificationTokenInfo(TenantIdentifier } @Override - public void revokeAllTokens(TenantIdentifier tenantIdentifier, String userId, String email) throws StorageQueryException { + public void revokeAllTokens(TenantIdentifier tenantIdentifier, String userId, String email) throws + StorageQueryException { try { EmailVerificationQueries.revokeAllTokens(this, tenantIdentifier, userId, email); } catch (SQLException e) { @@ -1038,11 +1069,13 @@ public void unverifyEmail(AppIdentifier appIdentifier, String userId, String ema } @Override - public EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUser(TenantIdentifier tenantIdentifier, + public EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUser(TenantIdentifier + tenantIdentifier, String userId, String email) throws StorageQueryException { try { - return EmailVerificationQueries.getAllEmailVerificationTokenInfoForUser(this, tenantIdentifier, userId, email); + return EmailVerificationQueries.getAllEmailVerificationTokenInfoForUser(this, tenantIdentifier, userId, + email); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1096,7 +1129,8 @@ public void updateUserEmail_Transaction(AppIdentifier appIdentifier, Transaction } @Override - public void signUp(TenantIdentifier tenantIdentifier, io.supertokens.pluginInterface.thirdparty.UserInfo userInfo) + public void signUp(TenantIdentifier tenantIdentifier, io.supertokens.pluginInterface.thirdparty.UserInfo + userInfo) throws StorageQueryException, io.supertokens.pluginInterface.thirdparty.exception.DuplicateUserIdException, DuplicateThirdPartyUserException, TenantOrAppNotFoundException { try { @@ -1107,7 +1141,8 @@ public void signUp(TenantIdentifier tenantIdentifier, io.supertokens.pluginInter PostgreSQLConfig config = Config.getConfig(this); ServerErrorMessage serverMessage = ((PSQLException) e).getServerErrorMessage(); - if (isUniqueConstraintError(serverMessage, config.getThirdPartyUserToTenantTable(), "third_party_user_id")) { + if (isUniqueConstraintError(serverMessage, config.getThirdPartyUserToTenantTable(), + "third_party_user_id")) { throw new DuplicateThirdPartyUserException(); } else if (isPrimaryKeyError(serverMessage, config.getThirdPartyUsersTable()) @@ -1146,7 +1181,8 @@ public io.supertokens.pluginInterface.thirdparty.UserInfo getThirdPartyUserInfoU String thirdPartyUserId) throws StorageQueryException { try { - return ThirdPartyQueries.getThirdPartyUserInfoUsingId(this, tenantIdentifier, thirdPartyId, thirdPartyUserId); + return ThirdPartyQueries.getThirdPartyUserInfoUsingId(this, tenantIdentifier, thirdPartyId, + thirdPartyUserId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1200,10 +1236,11 @@ public long getUsersCount(AppIdentifier appIdentifier, RECIPE_ID[] includeRecipe public AuthRecipeUserInfo[] getUsers(TenantIdentifier tenantIdentifier, @NotNull Integer limit, @NotNull String timeJoinedOrder, @Nullable RECIPE_ID[] includeRecipeIds, @Nullable String userId, - @Nullable Long timeJoined) + @Nullable Long timeJoined, @Nullable DashboardSearchTags dashboardSearchTags) throws StorageQueryException { try { - return GeneralQueries.getUsers(this, tenantIdentifier, limit, timeJoinedOrder, includeRecipeIds, userId, timeJoined); + return GeneralQueries.getUsers(this, tenantIdentifier, limit, timeJoinedOrder, includeRecipeIds, userId, + timeJoined, dashboardSearchTags); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1219,6 +1256,55 @@ public boolean doesUserIdExist(AppIdentifier appIdentifier, String userId) throw } } + public void updateLastActive(String userId) throws StorageQueryException { + try { + ActiveUsersQueries.updateUserLastActive(this, userId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public void updateLastActive(AppIdentifier appIdentifier, String userId) throws StorageQueryException { + try { + // TODO.. + ActiveUsersQueries.updateUserLastActive(this, userId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public int countUsersActiveSince(AppIdentifier appIdentifier, long time) throws StorageQueryException { + try { + // TODO... + return ActiveUsersQueries.countUsersActiveSince(this, time); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public int countUsersEnabledTotp(AppIdentifier appIdentifier) throws StorageQueryException { + try { + // TODO... + return ActiveUsersQueries.countUsersEnabledTotp(this); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public int countUsersEnabledTotpAndActiveSince(AppIdentifier appIdentifier, long time) + throws StorageQueryException { + try { + // TODO... + return ActiveUsersQueries.countUsersEnabledTotpAndActiveSince(this, time); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + @Override public boolean doesUserIdExist(TenantIdentifier tenantIdentifierIdentifier, String userId) throws StorageQueryException { @@ -1231,7 +1317,8 @@ public boolean doesUserIdExist(TenantIdentifier tenantIdentifierIdentifier, Stri } @Override - public List getJWTSigningKeys_Transaction(AppIdentifier appIdentifier, TransactionConnection con) + public List getJWTSigningKeys_Transaction(AppIdentifier + appIdentifier, TransactionConnection con) throws StorageQueryException { // TODO.. Connection sqlCon = (Connection) con.getConnection(); @@ -1267,12 +1354,14 @@ public void setJWTSigningKey_Transaction(AppIdentifier appIdentifier, Transactio } } - private boolean isUniqueConstraintError(ServerErrorMessage serverMessage, String tableName, String columnName) { + private boolean isUniqueConstraintError(ServerErrorMessage serverMessage, String tableName, String + columnName) { return serverMessage.getSQLState().equals("23505") && serverMessage.getConstraint() != null && serverMessage.getConstraint().equals(tableName + "_" + columnName + "_key"); } - private boolean isForeignKeyConstraintError(ServerErrorMessage serverMessage, String tableName, String columnName) { + private boolean isForeignKeyConstraintError(ServerErrorMessage serverMessage, String tableName, String + columnName) { return serverMessage.getSQLState().equals("23503") && serverMessage.getConstraint() != null && serverMessage.getConstraint().equals(tableName + "_" + columnName + "_fkey"); } @@ -1283,7 +1372,8 @@ private boolean isPrimaryKeyError(ServerErrorMessage serverMessage, String table } @Override - public PasswordlessDevice getDevice_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, + public PasswordlessDevice getDevice_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection + con, String deviceIdHash) throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); @@ -1300,7 +1390,8 @@ public void incrementDeviceFailedAttemptCount_Transaction(TenantIdentifier tenan throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { - PasswordlessQueries.incrementDeviceFailedAttemptCount_Transaction(this, sqlCon, tenantIdentifier, deviceIdHash); + PasswordlessQueries.incrementDeviceFailedAttemptCount_Transaction(this, sqlCon, tenantIdentifier, + deviceIdHash); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1308,7 +1399,8 @@ public void incrementDeviceFailedAttemptCount_Transaction(TenantIdentifier tenan } @Override - public PasswordlessCode[] getCodesOfDevice_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, + public PasswordlessCode[] getCodesOfDevice_Transaction(TenantIdentifier + tenantIdentifier, TransactionConnection con, String deviceIdHash) throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); @@ -1332,7 +1424,8 @@ public void deleteDevice_Transaction(TenantIdentifier tenantIdentifier, Transact } @Override - public void deleteDevicesByPhoneNumber_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, + public void deleteDevicesByPhoneNumber_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection + con, @Nonnull String phoneNumber) throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); @@ -1361,14 +1454,16 @@ public void deleteDevicesByPhoneNumber_Transaction(AppIdentifier appIdentifier, String phoneNumber, String userId) throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { - PasswordlessQueries.deleteDevicesByPhoneNumber_Transaction(this, sqlCon, appIdentifier, phoneNumber, userId); + PasswordlessQueries.deleteDevicesByPhoneNumber_Transaction(this, sqlCon, appIdentifier, phoneNumber, + userId); } catch (SQLException e) { throw new StorageQueryException(e); } } @Override - public void deleteDevicesByEmail_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String email, + public void deleteDevicesByEmail_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String + email, String userId) throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { @@ -1384,7 +1479,8 @@ public PasswordlessCode getCodeByLinkCodeHash_Transaction(TenantIdentifier tenan throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { - return PasswordlessQueries.getCodeByLinkCodeHash_Transaction(this, sqlCon, tenantIdentifier, linkCodeHash); + return PasswordlessQueries.getCodeByLinkCodeHash_Transaction(this, sqlCon, tenantIdentifier, + linkCodeHash); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1402,12 +1498,14 @@ public void deleteCode_Transaction(TenantIdentifier tenantIdentifier, Transactio } @Override - public void updateUserEmail_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId, + public void updateUserEmail_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String + userId, String email) throws StorageQueryException, UnknownUserIdException, DuplicateEmailException { Connection sqlCon = (Connection) con.getConnection(); try { - int updated_rows = PasswordlessQueries.updateUserEmail_Transaction(this, sqlCon, appIdentifier, userId, email); + int updated_rows = PasswordlessQueries.updateUserEmail_Transaction(this, sqlCon, appIdentifier, userId, + email); if (updated_rows != 1) { throw new UnknownUserIdException(); } @@ -1426,12 +1524,14 @@ public void updateUserEmail_Transaction(AppIdentifier appIdentifier, Transaction } @Override - public void updateUserPhoneNumber_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId, - String phoneNumber) + public void updateUserPhoneNumber_Transaction(AppIdentifier appIdentifier, TransactionConnection + con, String userId, String phoneNumber) throws StorageQueryException, UnknownUserIdException, DuplicatePhoneNumberException { Connection sqlCon = (Connection) con.getConnection(); try { - int updated_rows = PasswordlessQueries.updateUserPhoneNumber_Transaction(this, sqlCon, appIdentifier, userId, phoneNumber); + int updated_rows = PasswordlessQueries.updateUserPhoneNumber_Transaction(this, sqlCon, appIdentifier, + userId, + phoneNumber); if (updated_rows != 1) { throw new UnknownUserIdException(); @@ -1461,7 +1561,8 @@ public void createDeviceWithCode(TenantIdentifier tenantIdentifier, @Nullable St throw new IllegalArgumentException("Both email and phoneNumber can't be null"); } try { - PasswordlessQueries.createDeviceWithCode(this, tenantIdentifier, email, phoneNumber, linkCodeSalt, code); + PasswordlessQueries.createDeviceWithCode(this, tenantIdentifier, email, phoneNumber, linkCodeSalt, + code); } catch (StorageTransactionLogicException e) { Exception actualException = e.actualException; @@ -1517,7 +1618,8 @@ public void createCode(TenantIdentifier tenantIdentifier, PasswordlessCode code) } @Override - public void createUser(TenantIdentifier tenantIdentifier, io.supertokens.pluginInterface.passwordless.UserInfo user) + public void createUser(TenantIdentifier + tenantIdentifier, io.supertokens.pluginInterface.passwordless.UserInfo user) throws StorageQueryException, DuplicateEmailException, DuplicatePhoneNumberException, DuplicateUserIdException, TenantOrAppNotFoundException { @@ -1562,7 +1664,8 @@ public void createUser(TenantIdentifier tenantIdentifier, io.supertokens.pluginI } @Override - public void deletePasswordlessUser(AppIdentifier appIdentifier, String userId) throws StorageQueryException { + public void deletePasswordlessUser(AppIdentifier appIdentifier, String userId) throws + StorageQueryException { try { PasswordlessQueries.deleteUser(this, appIdentifier, userId); } catch (StorageTransactionLogicException e) { @@ -1591,7 +1694,8 @@ public PasswordlessDevice[] getDevicesByEmail(TenantIdentifier tenantIdentifier, } @Override - public PasswordlessDevice[] getDevicesByPhoneNumber(TenantIdentifier tenantIdentifier, String phoneNumber) + public PasswordlessDevice[] getDevicesByPhoneNumber(TenantIdentifier tenantIdentifier, String + phoneNumber) throws StorageQueryException { try { return PasswordlessQueries.getDevicesByPhoneNumber(this, tenantIdentifier, phoneNumber); @@ -1621,7 +1725,8 @@ public PasswordlessCode[] getCodesBefore(TenantIdentifier tenantIdentifier, long } @Override - public PasswordlessCode getCode(TenantIdentifier tenantIdentifier, String codeId) throws StorageQueryException { + public PasswordlessCode getCode(TenantIdentifier tenantIdentifier, String codeId) throws + StorageQueryException { try { return PasswordlessQueries.getCode(this, tenantIdentifier, codeId); } catch (SQLException e) { @@ -1651,7 +1756,8 @@ public io.supertokens.pluginInterface.passwordless.UserInfo getUserById(AppIdent } @Override - public io.supertokens.pluginInterface.passwordless.UserInfo getUserByEmail(TenantIdentifier tenantIdentifier, + public io.supertokens.pluginInterface.passwordless.UserInfo getUserByEmail(TenantIdentifier + tenantIdentifier, String email) throws StorageQueryException { try { @@ -1662,7 +1768,8 @@ public io.supertokens.pluginInterface.passwordless.UserInfo getUserByEmail(Tenan } @Override - public io.supertokens.pluginInterface.passwordless.UserInfo getUserByPhoneNumber(TenantIdentifier tenantIdentifier, + public io.supertokens.pluginInterface.passwordless.UserInfo getUserByPhoneNumber(TenantIdentifier + tenantIdentifier, String phoneNumber) throws StorageQueryException { try { @@ -1673,7 +1780,8 @@ public io.supertokens.pluginInterface.passwordless.UserInfo getUserByPhoneNumber } @Override - public JsonObject getUserMetadata(AppIdentifier appIdentifier, String userId) throws StorageQueryException { + public JsonObject getUserMetadata(AppIdentifier appIdentifier, String userId) throws + StorageQueryException { try { return UserMetadataQueries.getUserMetadata(this, appIdentifier, userId); } catch (SQLException e) { @@ -1682,7 +1790,8 @@ public JsonObject getUserMetadata(AppIdentifier appIdentifier, String userId) th } @Override - public JsonObject getUserMetadata_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId) + public JsonObject getUserMetadata_Transaction(AppIdentifier appIdentifier, TransactionConnection + con, String userId) throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { @@ -1693,12 +1802,14 @@ public JsonObject getUserMetadata_Transaction(AppIdentifier appIdentifier, Trans } @Override - public int setUserMetadata_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId, + public int setUserMetadata_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String + userId, JsonObject metadata) throws StorageQueryException, TenantOrAppNotFoundException { Connection sqlCon = (Connection) con.getConnection(); try { - return UserMetadataQueries.setUserMetadata_Transaction(this, sqlCon, appIdentifier, userId, metadata); + return UserMetadataQueries.setUserMetadata_Transaction(this, sqlCon, appIdentifier, userId, + metadata); } catch (SQLException e) { if (e instanceof PSQLException) { PostgreSQLConfig config = Config.getConfig(this); @@ -1737,7 +1848,8 @@ public void addRoleToUser(TenantIdentifier tenantIdentifier, String userId, Stri if (isPrimaryKeyError(serverErrorMessage, config.getUserRolesTable())) { throw new DuplicateUserRoleMappingException(); } - if (isForeignKeyConstraintError(serverErrorMessage, config.getUserRolesTable(), "tenant_id")) { + if (isForeignKeyConstraintError(serverErrorMessage, config.getUserRolesTable(), + "tenant_id")) { throw new TenantOrAppNotFoundException(tenantIdentifier); } } @@ -1747,7 +1859,8 @@ public void addRoleToUser(TenantIdentifier tenantIdentifier, String userId, Stri } @Override - public String[] getRolesForUser(TenantIdentifier tenantIdentifier, String userId) throws StorageQueryException { + public String[] getRolesForUser(TenantIdentifier tenantIdentifier, String userId) throws + StorageQueryException { try { return UserRolesQueries.getRolesForUser(this, tenantIdentifier, userId); } catch (SQLException e) { @@ -1755,7 +1868,8 @@ public String[] getRolesForUser(TenantIdentifier tenantIdentifier, String userId } } - private String[] getRolesForUser(AppIdentifier appIdentifier, String userId) throws StorageQueryException { + private String[] getRolesForUser(AppIdentifier appIdentifier, String userId) throws + StorageQueryException { try { return UserRolesQueries.getRolesForUser(this, appIdentifier, userId); } catch (SQLException e) { @@ -1764,7 +1878,8 @@ private String[] getRolesForUser(AppIdentifier appIdentifier, String userId) thr } @Override - public String[] getUsersForRole(TenantIdentifier tenantIdentifier, String role) throws StorageQueryException { + public String[] getUsersForRole(TenantIdentifier tenantIdentifier, String role) throws + StorageQueryException { try { return UserRolesQueries.getUsersForRole(this, tenantIdentifier, role); } catch (SQLException e) { @@ -1773,7 +1888,8 @@ public String[] getUsersForRole(TenantIdentifier tenantIdentifier, String role) } @Override - public String[] getPermissionsForRole(AppIdentifier appIdentifier, String role) throws StorageQueryException { + public String[] getPermissionsForRole(AppIdentifier appIdentifier, String role) throws + StorageQueryException { try { return UserRolesQueries.getPermissionsForRole(this, appIdentifier, role); } catch (SQLException e) { @@ -1819,7 +1935,8 @@ public boolean doesRoleExist(AppIdentifier appIdentifier, String role) throws St } @Override - public int deleteAllRolesForUser(TenantIdentifier tenantIdentifier, String userId) throws StorageQueryException { + public int deleteAllRolesForUser(TenantIdentifier tenantIdentifier, String userId) throws + StorageQueryException { try { return UserRolesQueries.deleteAllRolesForUser(this, tenantIdentifier, userId); } catch (SQLException e) { @@ -1828,7 +1945,8 @@ public int deleteAllRolesForUser(TenantIdentifier tenantIdentifier, String userI } @Override - public void deleteAllRolesForUser(AppIdentifier appIdentifier, String userId) throws StorageQueryException { + public void deleteAllRolesForUser(AppIdentifier appIdentifier, String userId) throws + StorageQueryException { try { UserRolesQueries.deleteAllRolesForUser(this, appIdentifier, userId); } catch (SQLException e) { @@ -1837,13 +1955,15 @@ public void deleteAllRolesForUser(AppIdentifier appIdentifier, String userId) th } @Override - public boolean deleteRoleForUser_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, + public boolean deleteRoleForUser_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection + con, String userId, String role) throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { - return UserRolesQueries.deleteRoleForUser_Transaction(this, sqlCon, tenantIdentifier, userId, role); + return UserRolesQueries.deleteRoleForUser_Transaction(this, sqlCon, tenantIdentifier, userId, + role); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1884,7 +2004,8 @@ public void addPermissionToRoleOrDoNothingIfExists_Transaction(AppIdentifier app if (e instanceof PSQLException) { PostgreSQLConfig config = Config.getConfig(this); ServerErrorMessage serverErrorMessage = ((PSQLException) e).getServerErrorMessage(); - if (isForeignKeyConstraintError(serverErrorMessage, config.getUserRolesPermissionsTable(), "role")) { + if (isForeignKeyConstraintError(serverErrorMessage, config.getUserRolesPermissionsTable(), + "role")) { throw new UnknownRoleException(); } } @@ -1894,31 +2015,36 @@ public void addPermissionToRoleOrDoNothingIfExists_Transaction(AppIdentifier app } @Override - public boolean deletePermissionForRole_Transaction(AppIdentifier appIdentifier, TransactionConnection con, + public boolean deletePermissionForRole_Transaction(AppIdentifier appIdentifier, TransactionConnection + con, String role, String permission) throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { - return UserRolesQueries.deletePermissionForRole_Transaction(this, sqlCon, appIdentifier, role, permission); + return UserRolesQueries.deletePermissionForRole_Transaction(this, sqlCon, appIdentifier, role, + permission); } catch (SQLException e) { throw new StorageQueryException(e); } } @Override - public int deleteAllPermissionsForRole_Transaction(AppIdentifier appIdentifier, TransactionConnection con, + public int deleteAllPermissionsForRole_Transaction(AppIdentifier appIdentifier, TransactionConnection + con, String role) throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { - return UserRolesQueries.deleteAllPermissionsForRole_Transaction(this, sqlCon, appIdentifier, role); + return UserRolesQueries.deleteAllPermissionsForRole_Transaction(this, sqlCon, appIdentifier, + role); } catch (SQLException e) { throw new StorageQueryException(e); } } @Override - public boolean doesRoleExist_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String role) + public boolean doesRoleExist_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String + role) throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { @@ -1929,12 +2055,14 @@ public boolean doesRoleExist_Transaction(AppIdentifier appIdentifier, Transactio } @Override - public void createUserIdMapping(AppIdentifier appIdentifier, String superTokensUserId, String externalUserId, + public void createUserIdMapping(AppIdentifier appIdentifier, String superTokensUserId, String + externalUserId, @Nullable String externalUserIdInfo) throws StorageQueryException, UnknownSuperTokensUserIdException, UserIdMappingAlreadyExistsException { // TODO.. try { - UserIdMappingQueries.createUserIdMapping(this, appIdentifier, superTokensUserId, externalUserId, externalUserIdInfo); + UserIdMappingQueries.createUserIdMapping(this, appIdentifier, superTokensUserId, externalUserId, + externalUserIdInfo); } catch (SQLException e) { if (e instanceof PSQLException) { PostgreSQLConfig config = Config.getConfig(this); @@ -1953,7 +2081,8 @@ public void createUserIdMapping(AppIdentifier appIdentifier, String superTokensU throw new UserIdMappingAlreadyExistsException(true, false); } - if (isUniqueConstraintError(serverErrorMessage, config.getUserIdMappingTable(), "external_user_id")) { + if (isUniqueConstraintError(serverErrorMessage, config.getUserIdMappingTable(), + "external_user_id")) { throw new UserIdMappingAlreadyExistsException(false, true); } } @@ -1963,12 +2092,14 @@ public void createUserIdMapping(AppIdentifier appIdentifier, String superTokensU } @Override - public boolean deleteUserIdMapping(AppIdentifier appIdentifier, String userId, boolean isSuperTokensUserId) + public boolean deleteUserIdMapping(AppIdentifier appIdentifier, String userId, + boolean isSuperTokensUserId) throws StorageQueryException { // TODO.. try { if (isSuperTokensUserId) { - return UserIdMappingQueries.deleteUserIdMappingWithSuperTokensUserId(this, appIdentifier, userId); + return UserIdMappingQueries.deleteUserIdMappingWithSuperTokensUserId(this, appIdentifier, + userId); } return UserIdMappingQueries.deleteUserIdMappingWithExternalUserId(this, appIdentifier, userId); @@ -1978,12 +2109,14 @@ public boolean deleteUserIdMapping(AppIdentifier appIdentifier, String userId, b } @Override - public UserIdMapping getUserIdMapping(AppIdentifier appIdentifier, String userId, boolean isSuperTokensUserId) + public UserIdMapping getUserIdMapping(AppIdentifier appIdentifier, String userId, + boolean isSuperTokensUserId) throws StorageQueryException { // TODO.. try { if (isSuperTokensUserId) { - return UserIdMappingQueries.getuseraIdMappingWithSuperTokensUserId(this, appIdentifier, userId); + return UserIdMappingQueries.getuseraIdMappingWithSuperTokensUserId(this, appIdentifier, + userId); } return UserIdMappingQueries.getUserIdMappingWithExternalUserId(this, appIdentifier, userId); @@ -1997,7 +2130,9 @@ public UserIdMapping[] getUserIdMapping(AppIdentifier appIdentifier, String user throws StorageQueryException { // TODO.. try { - return UserIdMappingQueries.getUserIdMappingWithEitherSuperTokensUserIdOrExternalUserId(this, appIdentifier, userId); + return UserIdMappingQueries.getUserIdMappingWithEitherSuperTokensUserIdOrExternalUserId(this, + appIdentifier, + userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -2026,7 +2161,6 @@ public boolean updateOrDeleteExternalUserIdInfo(AppIdentifier appIdentifier, Str public HashMap getUserIdMappingForSuperTokensIds(ArrayList userIds) throws StorageQueryException { try { - return UserIdMappingQueries.getUserIdMappingWithUserIds(this, userIds); } catch (SQLException e) { throw new StorageQueryException(e); @@ -2221,18 +2355,9 @@ public boolean revokeSessionWithSessionId(AppIdentifier appIdentifier, String se } @Override - public void revokeExpiredSessions() throws StorageQueryException { - try { - DashboardQueries.deleteExpiredSessions(this); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - - } - - @Override - public void updateDashboardUsersEmailWithUserId_Transaction(AppIdentifier appIdentifier, TransactionConnection - con, String userId, + public void updateDashboardUsersEmailWithUserId_Transaction(AppIdentifier + appIdentifier, TransactionConnection + con, String userId, String newEmail) throws StorageQueryException, io.supertokens.pluginInterface.dashboard.exceptions.DuplicateEmailException, UserIdNotFoundException { @@ -2304,7 +2429,8 @@ public void createNewDashboardUser(AppIdentifier appIdentifier, DashboardUser us io.supertokens.pluginInterface.dashboard.exceptions.DuplicateEmailException { // TODO.. try { - DashboardQueries.createDashboardUser(this, userInfo.userId, userInfo.email, userInfo.passwordHash, + DashboardQueries.createDashboardUser(this, userInfo.userId, userInfo.email, + userInfo.passwordHash, userInfo.timeJoined); } catch (SQLException e) { if (e instanceof PSQLException) { @@ -2335,4 +2461,166 @@ public DashboardUser getDashboardUserByEmail(AppIdentifier appIdentifier, String throw new StorageQueryException(e); } } + + @Override + public void revokeExpiredSessions() throws StorageQueryException { + try { + DashboardQueries.deleteExpiredSessions(this); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + + } + + // TOTP recipe: + @Override + public void createDevice(AppIdentifier appIdentifier, TOTPDevice device) + throws StorageQueryException, DeviceAlreadyExistsException { + try { + // TODO.. + TOTPQueries.createDevice(this, device); + } catch (StorageTransactionLogicException e) { + Exception actualException = e.actualException; + + if (actualException instanceof PSQLException) { + ServerErrorMessage errMsg = ((PSQLException) actualException).getServerErrorMessage(); + + if (isPrimaryKeyError(errMsg, Config.getConfig(this).getTotpUserDevicesTable())) { + throw new DeviceAlreadyExistsException(); + } + } + + throw new StorageQueryException(e.actualException); + } + } + + @Override + public void markDeviceAsVerified(AppIdentifier appIdentifier, String userId, String deviceName) + throws StorageQueryException, UnknownDeviceException { + try { + // TODO.. + int matchedCount = TOTPQueries.markDeviceAsVerified(this, userId, deviceName); + if (matchedCount == 0) { + // Note matchedCount != updatedCount + throw new UnknownDeviceException(); + } + return; // Device was marked as verified + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public int deleteDevice_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId, + String deviceName) + throws StorageQueryException { + // TODO.. + Connection sqlCon = (Connection) con.getConnection(); + try { + return TOTPQueries.deleteDevice_Transaction(this, sqlCon, userId, deviceName); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public void removeUser_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId) + throws StorageQueryException { + // TODO.. + Connection sqlCon = (Connection) con.getConnection(); + try { + TOTPQueries.removeUser_Transaction(this, sqlCon, userId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public void updateDeviceName(AppIdentifier appIdentifier, String userId, String oldDeviceName, String newDeviceName) + throws StorageQueryException, DeviceAlreadyExistsException, + UnknownDeviceException { + // TODO.. + try { + int updatedCount = TOTPQueries.updateDeviceName(this, userId, oldDeviceName, newDeviceName); + if (updatedCount == 0) { + throw new UnknownDeviceException(); + } + } catch (SQLException e) { + if (e instanceof PSQLException) { + ServerErrorMessage errMsg = ((PSQLException) e).getServerErrorMessage(); + if (isPrimaryKeyError(errMsg, Config.getConfig(this).getTotpUserDevicesTable())) { + throw new DeviceAlreadyExistsException(); + } + } + } + } + + @Override + public TOTPDevice[] getDevices(AppIdentifier appIdentifier, String userId) + throws StorageQueryException { + // TODO.. + try { + return TOTPQueries.getDevices(this, userId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public TOTPDevice[] getDevices_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId) + throws StorageQueryException { + // TODO.. + Connection sqlCon = (Connection) con.getConnection(); + try { + return TOTPQueries.getDevices_Transaction(this, sqlCon, userId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public void insertUsedCode_Transaction(TransactionConnection con, TenantIdentifier tenantIdentifier, + TOTPUsedCode usedCodeObj) + throws StorageQueryException, TotpNotEnabledException, UsedCodeAlreadyExistsException { + // TODO.. + Connection sqlCon = (Connection) con.getConnection(); + try { + TOTPQueries.insertUsedCode_Transaction(this, sqlCon, usedCodeObj); + } catch (SQLException e) { + ServerErrorMessage err = ((PSQLException) e).getServerErrorMessage(); + + if (isPrimaryKeyError(err, Config.getConfig(this).getTotpUsedCodesTable())) { + throw new UsedCodeAlreadyExistsException(); + } else if (isForeignKeyConstraintError(err, Config.getConfig(this).getTotpUsedCodesTable(), + "user_id")) { + throw new TotpNotEnabledException(); + } + + throw new StorageQueryException(e); + } + } + + @Override + public TOTPUsedCode[] getAllUsedCodesDescOrder_Transaction(TransactionConnection con, + TenantIdentifier tenantIdentifier, String userId) + throws StorageQueryException { + // TODO.. + Connection sqlCon = (Connection) con.getConnection(); + try { + return TOTPQueries.getAllUsedCodesDescOrder_Transaction(this, sqlCon, userId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public int removeExpiredCodes(TenantIdentifier tenantIdentifier, long expiredBefore) + throws StorageQueryException { + // TODO.. + try { + return TOTPQueries.removeExpiredCodes(this, expiredBefore); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } } diff --git a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java index 265c400b..de7020e0 100644 --- a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java +++ b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java @@ -100,7 +100,8 @@ public String getConnectionScheme() { if (postgresql_connection_uri != null) { URI uri = URI.create(postgresql_connection_uri); - // sometimes if the scheme is missing, the host is returned as the scheme. To prevent that, + // sometimes if the scheme is missing, the host is returned as the scheme. To + // prevent that, // we have a check String host = this.getHostName(); if (uri.getScheme() != null && !uri.getScheme().equals(host)) { @@ -253,6 +254,10 @@ public String getAppIdToUserIdTable() { return addSchemaAndPrefixToTableName("app_id_to_user_id"); } + public String getUserLastActiveTable() { + return addSchemaAndPrefixToTableName("user_last_active"); + } + public String getAccessTokenSigningKeysTable() { return addSchemaAndPrefixToTableName("session_access_token_signing_keys"); } @@ -371,6 +376,18 @@ public String getDashboardSessionsTable() { return addSchemaAndPrefixToTableName("dashboard_user_sessions"); } + public String getTotpUsersTable() { + return addSchemaAndPrefixToTableName("totp_users"); + } + + public String getTotpUserDevicesTable() { + return addSchemaAndPrefixToTableName("totp_user_devices"); + } + + public String getTotpUsedCodesTable() { + return addSchemaAndPrefixToTableName("totp_used_codes"); + } + private String addSchemaAndPrefixToTableName(String tableName) { String name = tableName; if (!getTablePrefix().equals("")) { @@ -461,4 +478,4 @@ void assertThatConfigFromSameUserPoolIsNotConflicting(PostgreSQLConfig otherConf " for the same user pool"); } } -} \ No newline at end of file +} diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java new file mode 100644 index 00000000..1be94685 --- /dev/null +++ b/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java @@ -0,0 +1,68 @@ +package io.supertokens.storage.postgresql.queries; + +import java.sql.SQLException; + +import io.supertokens.storage.postgresql.Start; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.storage.postgresql.config.Config; + +import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; +import static io.supertokens.storage.postgresql.QueryExecutorTemplate.update; + +public class ActiveUsersQueries { + static String getQueryToCreateUserLastActiveTable(Start start) { + return "CREATE TABLE IF NOT EXISTS " + Config.getConfig(start).getUserLastActiveTable() + " (" + + "user_id VARCHAR(128)," + + "last_active_time BIGINT," + "PRIMARY KEY(user_id)" + " );"; + } + + public static int countUsersActiveSince(Start start, long sinceTime) throws SQLException, StorageQueryException { + String QUERY = "SELECT COUNT(*) as total FROM " + Config.getConfig(start).getUserLastActiveTable() + + " WHERE last_active_time >= ?"; + + return execute(start, QUERY, pst -> pst.setLong(1, sinceTime), result -> { + if (result.next()) { + return result.getInt("total"); + } + return 0; + }); + } + + + public static int countUsersEnabledTotp(Start start) throws SQLException, StorageQueryException { + String QUERY = "SELECT COUNT(*) as total FROM " + Config.getConfig(start).getTotpUsersTable(); + + return execute(start, QUERY, null, result -> { + if (result.next()) { + return result.getInt("total"); + } + return 0; + }); + } + + public static int countUsersEnabledTotpAndActiveSince(Start start, 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.last_active_time >= ?"; + + return execute(start, QUERY, pst -> pst.setLong(1, sinceTime), result -> { + if (result.next()) { + return result.getInt("total"); + } + return 0; + }); + } + + public static int updateUserLastActive(Start start, String userId) throws SQLException, StorageQueryException { + String QUERY = "INSERT INTO " + Config.getConfig(start).getUserLastActiveTable() + + "(user_id, last_active_time) VALUES(?, ?) ON CONFLICT(user_id) DO UPDATE SET last_active_time = ?"; + + long now = System.currentTimeMillis(); + return update(start, QUERY, pst -> { + pst.setString(1, userId); + pst.setLong(2, now); + pst.setLong(3, now); + }); + } +} diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java index b2ed6aab..6cdc6f81 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java @@ -20,9 +20,8 @@ import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.RowMapper; import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +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.TenantIdentifier; import io.supertokens.storage.postgresql.ConnectionPool; import io.supertokens.storage.postgresql.Start; @@ -81,10 +80,11 @@ static String getQueryToCreateUsersTable(Start start) { + " PRIMARY KEY (app_id, tenant_id, user_id)," + "CONSTRAINT " + Utils.getConstraintName(schema, usersTable, "tenant_id", "fkey") + " FOREIGN KEY(app_id, tenant_id)" - + " REFERENCES " + Config.getConfig(start).getTenantsTable() + " (app_id, tenant_id) ON DELETE CASCADE," + + " REFERENCES " + Config.getConfig(start).getTenantsTable() + " (app_id, tenant_id) ON DELETE CASCADE," + "CONSTRAINT " + Utils.getConstraintName(schema, usersTable, "user_id", "fkey") + " FOREIGN KEY(app_id, user_id)" - + " REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable() + " (app_id, user_id) ON DELETE CASCADE" + + " REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable() + + " (app_id, user_id) ON DELETE CASCADE" + ");"; // @formatter:on } @@ -192,6 +192,11 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto update(start, getQueryToCreateUserPaginationIndex(start), NO_OP_SETTER); } + if (!doesTableExists(start, Config.getConfig(start).getUserLastActiveTable())) { + getInstance(start).addState(CREATING_NEW_TABLE, null); + update(start, ActiveUsersQueries.getQueryToCreateUserLastActiveTable(start), NO_OP_SETTER); + } + if (!doesTableExists(start, Config.getConfig(start).getAccessTokenSigningKeysTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); update(start, getQueryToCreateAccessTokenSigningKeysTable(start), NO_OP_SETTER); @@ -226,7 +231,8 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto if (!doesTableExists(start, Config.getConfig(start).getEmailPasswordUserToTenantTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, EmailPasswordQueries.getQueryToCreateEmailPasswordUserToTenantTable(start), NO_OP_SETTER); + update(start, EmailPasswordQueries.getQueryToCreateEmailPasswordUserToTenantTable(start), + NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getPasswordResetTokensTable())) { @@ -273,7 +279,8 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto if (!doesTableExists(start, Config.getConfig(start).getPasswordlessUserToTenantTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, PasswordlessQueries.getQueryToCreatePasswordlessUserToTenantTable(start), NO_OP_SETTER); + update(start, PasswordlessQueries.getQueryToCreatePasswordlessUserToTenantTable(start), + NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getPasswordlessDevicesTable())) { @@ -340,6 +347,23 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto NO_OP_SETTER); } + if (!doesTableExists(start, Config.getConfig(start).getTotpUsersTable())) { + getInstance(start).addState(CREATING_NEW_TABLE, null); + update(start, TOTPQueries.getQueryToCreateUsersTable(start), NO_OP_SETTER); + } + + if (!doesTableExists(start, Config.getConfig(start).getTotpUserDevicesTable())) { + getInstance(start).addState(CREATING_NEW_TABLE, null); + update(start, TOTPQueries.getQueryToCreateUserDevicesTable(start), NO_OP_SETTER); + } + + if (!doesTableExists(start, Config.getConfig(start).getTotpUsedCodesTable())) { + getInstance(start).addState(CREATING_NEW_TABLE, null); + update(start, TOTPQueries.getQueryToCreateUsedCodesTable(start), NO_OP_SETTER); + // index: + update(start, TOTPQueries.getQueryToCreateUsedCodesExpiryTimeIndex(start), NO_OP_SETTER); + } + } catch (Exception e) { if (e.getMessage().contains("schema") && e.getMessage().contains("does not exist") && numberOfRetries < 1) { @@ -376,6 +400,7 @@ public static void deleteAllTables(Start start) throws SQLException, StorageQuer { String DROP_QUERY = "DROP TABLE IF EXISTS " + getConfig(start).getAppsTable() + "," + + getConfig(start).getUserLastActiveTable() + "," + getConfig(start).getTenantsTable() + "," + getConfig(start).getKeyValueTable() + "," + getConfig(start).getAppIdToUserIdTable() + "," @@ -403,7 +428,9 @@ public static void deleteAllTables(Start start) throws SQLException, StorageQuer + getConfig(start).getUserRolesPermissionsTable() + "," + getConfig(start).getUserRolesTable() + "," + getConfig(start).getDashboardUsersTable() + "," - + getConfig(start).getDashboardSessionsTable(); + + getConfig(start).getDashboardSessionsTable() + "," + + getConfig(start).getTotpUsedCodesTable() + "," + getConfig(start).getTotpUserDevicesTable() + "," + + getConfig(start).getTotpUsersTable(); update(start, DROP_QUERY, NO_OP_SETTER); } } @@ -508,15 +535,178 @@ public static boolean doesUserIdExist(Start start, String userId) throws SQLExce } - public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenantIdentifier, @NotNull Integer limit, @NotNull String timeJoinedOrder, + public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenantIdentifier, @NotNull Integer limit, + @NotNull String timeJoinedOrder, @Nullable RECIPE_ID[] includeRecipeIds, @Nullable String userId, - @Nullable Long timeJoined) + @Nullable Long timeJoined, + @Nullable DashboardSearchTags dashboardSearchTags) throws SQLException, StorageQueryException { // This list will be used to keep track of the result's order from the db List usersFromQuery; - { + if (dashboardSearchTags != null) { + ArrayList queryList = new ArrayList<>(); + { + StringBuilder USER_SEARCH_TAG_CONDITION = new StringBuilder(); + + { + // check if we should search through the emailpassword table + if (dashboardSearchTags.shouldEmailPasswordTableBeSearched()) { + String QUERY = "SELECT allAuthUsersTable.*" + " FROM " + getConfig(start).getUsersTable() + + " AS allAuthUsersTable" + + " JOIN " + getConfig(start).getEmailPasswordUsersTable() + + " AS emailpasswordTable ON allAuthUsersTable.user_id = emailpasswordTable.user_id"; + + // attach email tags to queries + QUERY = QUERY + " WHERE emailpasswordTable.email LIKE ? OR emailpasswordTable.email LIKE ?"; + queryList.add(dashboardSearchTags.emails.get(0) + "%"); + queryList.add("%@" + dashboardSearchTags.emails.get(0) + "%"); + for (int i = 1; i < dashboardSearchTags.emails.size(); i++) { + QUERY += " OR emailpasswordTable.email LIKE ? OR emailpasswordTable.email LIKE ?"; + queryList.add(dashboardSearchTags.emails.get(i) + "%"); + queryList.add("%@" + dashboardSearchTags.emails.get(i) + "%"); + } + + USER_SEARCH_TAG_CONDITION.append("SELECT * FROM ( ").append(QUERY) + .append(" LIMIT 1000) AS emailpasswordResultTable"); + } + } + + { + // check if we should search through the thirdparty table + if (dashboardSearchTags.shouldThirdPartyTableBeSearched()) { + String QUERY = "SELECT allAuthUsersTable.*" + " FROM " + getConfig(start).getUsersTable() + + " AS allAuthUsersTable" + + " JOIN " + getConfig(start).getThirdPartyUsersTable() + + " AS thirdPartyTable ON allAuthUsersTable.user_id = thirdPartyTable.user_id"; + + // check if email tag is present + if (dashboardSearchTags.emails != null) { + + QUERY += " WHERE ( thirdPartyTable.email LIKE ? OR thirdPartyTable.email LIKE ?"; + queryList.add(dashboardSearchTags.emails.get(0) + "%"); + queryList.add("%@" + dashboardSearchTags.emails.get(0) + "%"); + + for (int i = 1; i < dashboardSearchTags.emails.size(); i++) { + QUERY += " OR thirdPartyTable.email LIKE ? OR thirdPartyTable.email LIKE ?"; + queryList.add(dashboardSearchTags.emails.get(i) + "%"); + queryList.add("%@" + dashboardSearchTags.emails.get(i) + "%"); + } + + QUERY += " )"; + + } + + // check if providers tag is present + if (dashboardSearchTags.providers != null) { + if (dashboardSearchTags.emails != null) { + QUERY += " AND "; + } else { + QUERY += " WHERE "; + } + + QUERY += " ( thirdPartyTable.third_party_id LIKE ?"; + queryList.add(dashboardSearchTags.providers.get(0) + "%"); + for (int i = 1; i < dashboardSearchTags.providers.size(); i++) { + QUERY += " OR thirdPartyTable.third_party_id LIKE ?"; + queryList.add(dashboardSearchTags.providers.get(i) + "%"); + } + + QUERY += " )"; + } + + // check if we need to append this to an existing search query + if (USER_SEARCH_TAG_CONDITION.length() != 0) { + USER_SEARCH_TAG_CONDITION.append(" UNION ").append("SELECT * FROM ( ").append(QUERY) + .append(" LIMIT 1000) AS thirdPartyResultTable"); + + } else { + USER_SEARCH_TAG_CONDITION.append("SELECT * FROM ( ").append(QUERY) + .append(" LIMIT 1000) AS thirdPartyResultTable"); + + } + } + } + + { + // check if we should search through the passwordless table + if (dashboardSearchTags.shouldPasswordlessTableBeSearched()) { + String QUERY = "SELECT allAuthUsersTable.*" + " FROM " + getConfig(start).getUsersTable() + + " AS allAuthUsersTable" + + " JOIN " + getConfig(start).getPasswordlessUsersTable() + + " AS passwordlessTable ON allAuthUsersTable.user_id = passwordlessTable.user_id"; + + // check if email tag is present + if (dashboardSearchTags.emails != null) { + + QUERY = QUERY + " WHERE ( passwordlessTable.email LIKE ? OR passwordlessTable.email LIKE ?"; + queryList.add(dashboardSearchTags.emails.get(0) + "%"); + queryList.add("%@" + dashboardSearchTags.emails.get(0) + "%"); + for (int i = 1; i < dashboardSearchTags.emails.size(); i++) { + QUERY += " OR passwordlessTable.email LIKE ? OR passwordlessTable.email LIKE ?"; + queryList.add(dashboardSearchTags.emails.get(i) + "%"); + queryList.add("%@" + dashboardSearchTags.emails.get(i) + "%"); + } + + QUERY += " )"; + } + + // check if phone tag is present + if (dashboardSearchTags.phoneNumbers != null) { + + if (dashboardSearchTags.emails != null) { + QUERY += " AND "; + } else { + QUERY += " WHERE "; + } + + QUERY += " ( passwordlessTable.phone_number LIKE ?"; + queryList.add(dashboardSearchTags.phoneNumbers.get(0) + "%"); + for (int i = 1; i < dashboardSearchTags.phoneNumbers.size(); i++) { + QUERY += " OR passwordlessTable.phone_number LIKE ?"; + queryList.add(dashboardSearchTags.phoneNumbers.get(i) + "%"); + } + + QUERY += " )"; + } + + // check if we need to append this to an existing search query + if (USER_SEARCH_TAG_CONDITION.length() != 0) { + USER_SEARCH_TAG_CONDITION.append(" UNION ").append("SELECT * FROM ( ").append(QUERY) + .append(" LIMIT 1000) AS passwordlessResultTable"); + + } else { + USER_SEARCH_TAG_CONDITION.append("SELECT * FROM ( ").append(QUERY) + .append(" LIMIT 1000) AS passwordlessResultTable"); + + } + } + } + + if (USER_SEARCH_TAG_CONDITION.toString().length() == 0) { + usersFromQuery = new ArrayList<>(); + } else { + + String finalQuery = "SELECT * FROM ( " + USER_SEARCH_TAG_CONDITION.toString() + " )" + + " AS finalResultTable ORDER BY time_joined " + timeJoinedOrder + ", user_id DESC "; + usersFromQuery = execute(start, finalQuery, pst -> { + for (int i = 1; i <= queryList.size(); i++) { + pst.setString(i, queryList.get(i - 1)); + } + }, result -> { + List temp = new ArrayList<>(); + while (result.next()) { + temp.add(new UserInfoPaginationResultHolder(result.getString("user_id"), + result.getString("recipe_id"))); + } + return temp; + }); + } + + } + + } else { StringBuilder RECIPE_ID_CONDITION = new StringBuilder(); if (includeRecipeIds != null && includeRecipeIds.length > 0) { RECIPE_ID_CONDITION.append("recipe_id IN ("); @@ -610,7 +800,9 @@ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenant return finalResult; } - private static List getUserInfoForRecipeIdFromUserIds(Start start, TenantIdentifier tenantIdentifier, RECIPE_ID recipeId, + private static List getUserInfoForRecipeIdFromUserIds(Start start, + TenantIdentifier tenantIdentifier, + RECIPE_ID recipeId, List userIds) throws StorageQueryException, SQLException { if (recipeId == RECIPE_ID.EMAIL_PASSWORD) { diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/TOTPQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/TOTPQueries.java new file mode 100644 index 00000000..f4151ac4 --- /dev/null +++ b/src/main/java/io/supertokens/storage/postgresql/queries/TOTPQueries.java @@ -0,0 +1,255 @@ +package io.supertokens.storage.postgresql.queries; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import io.supertokens.storage.postgresql.Start; +import io.supertokens.storage.postgresql.config.Config; +import io.supertokens.pluginInterface.RowMapper; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; +import io.supertokens.pluginInterface.totp.TOTPDevice; +import io.supertokens.pluginInterface.totp.TOTPUsedCode; + +import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; +import static io.supertokens.storage.postgresql.QueryExecutorTemplate.update; + +public class TOTPQueries { + public static String getQueryToCreateUsersTable(Start start) { + return "CREATE TABLE IF NOT EXISTS " + Config.getConfig(start).getTotpUsersTable() + " (" + + "user_id VARCHAR(128) NOT NULL," + + "PRIMARY KEY (user_id))"; + } + + public static String getQueryToCreateUserDevicesTable(Start start) { + return "CREATE TABLE IF NOT EXISTS " + Config.getConfig(start).getTotpUserDevicesTable() + " (" + + "user_id VARCHAR(128) NOT NULL," + "device_name VARCHAR(256) NOT NULL," + + "secret_key VARCHAR(256) NOT NULL," + + "period INTEGER NOT NULL," + "skew INTEGER NOT NULL," + "verified BOOLEAN NOT NULL," + + "PRIMARY KEY (user_id, device_name)," + + "FOREIGN KEY (user_id) REFERENCES " + + Config.getConfig(start).getTotpUsersTable() + "(user_id) ON DELETE CASCADE);"; + } + + public static String getQueryToCreateUsedCodesTable(Start start) { + return "CREATE TABLE IF NOT EXISTS " + Config.getConfig(start).getTotpUsedCodesTable() + " (" + + "user_id VARCHAR(128) NOT NULL, " + + "code VARCHAR(8) NOT NULL," + "is_valid BOOLEAN NOT NULL," + + "expiry_time_ms BIGINT NOT NULL," + + "created_time_ms BIGINT NOT NULL," + + "PRIMARY KEY (user_id, created_time_ms)," + + "FOREIGN KEY (user_id) REFERENCES " + + Config.getConfig(start).getTotpUsersTable() + "(user_id) ON DELETE CASCADE);"; + } + + public static String getQueryToCreateUsedCodesExpiryTimeIndex(Start start) { + return "CREATE INDEX IF NOT EXISTS totp_used_codes_expiry_time_ms_index ON " + + Config.getConfig(start).getTotpUsedCodesTable() + " (expiry_time_ms)"; + } + + private static int insertUser_Transaction(Start start, Connection con, String userId) + throws SQLException, StorageQueryException { + // Create user if not exists: + String QUERY = "INSERT INTO " + Config.getConfig(start).getTotpUsersTable() + + " (user_id) VALUES (?) ON CONFLICT DO NOTHING"; + + return update(con, QUERY, pst -> pst.setString(1, userId)); + } + + private static int insertDevice_Transaction(Start start, Connection con, TOTPDevice device) + throws SQLException, StorageQueryException { + String QUERY = "INSERT INTO " + Config.getConfig(start).getTotpUserDevicesTable() + + " (user_id, device_name, secret_key, period, skew, verified) VALUES (?, ?, ?, ?, ?, ?)"; + + return update(con, QUERY, pst -> { + pst.setString(1, device.userId); + pst.setString(2, device.deviceName); + pst.setString(3, device.secretKey); + pst.setInt(4, device.period); + pst.setInt(5, device.skew); + pst.setBoolean(6, device.verified); + }); + } + + public static void createDevice(Start start, TOTPDevice device) + throws StorageQueryException, StorageTransactionLogicException { + start.startTransaction(con -> { + Connection sqlCon = (Connection) con.getConnection(); + + try { + insertUser_Transaction(start, sqlCon, device.userId); + insertDevice_Transaction(start, sqlCon, device); + sqlCon.commit(); + } catch (SQLException e) { + throw new StorageTransactionLogicException(e); + } + + return null; + }); + return; + } + + public static int markDeviceAsVerified(Start start, String userId, String deviceName) + throws StorageQueryException, SQLException { + String QUERY = "UPDATE " + Config.getConfig(start).getTotpUserDevicesTable() + + " SET verified = true WHERE user_id = ? AND device_name = ?"; + return update(start, QUERY, pst -> { + pst.setString(1, userId); + pst.setString(2, deviceName); + }); + } + + public static int deleteDevice_Transaction(Start start, Connection con, String userId, String deviceName) + throws SQLException, StorageQueryException { + String QUERY = "DELETE FROM " + Config.getConfig(start).getTotpUserDevicesTable() + + " WHERE user_id = ? AND device_name = ?;"; + + return update(con, QUERY, pst -> { + pst.setString(1, userId); + pst.setString(2, deviceName); + }); + } + + public static int removeUser_Transaction(Start start, Connection con, String userId) + throws SQLException, StorageQueryException { + String QUERY = "DELETE FROM " + Config.getConfig(start).getTotpUsersTable() + + " WHERE user_id = ?;"; + int removedUsersCount = update(con, QUERY, pst -> pst.setString(1, userId)); + + return removedUsersCount; + } + + public static int updateDeviceName(Start start, String userId, String oldDeviceName, String newDeviceName) + throws StorageQueryException, SQLException { + String QUERY = "UPDATE " + Config.getConfig(start).getTotpUserDevicesTable() + + " SET device_name = ? WHERE user_id = ? AND device_name = ?;"; + + return update(start, QUERY, pst -> { + pst.setString(1, newDeviceName); + pst.setString(2, userId); + pst.setString(3, oldDeviceName); + }); + } + + public static TOTPDevice[] getDevices(Start start, String userId) + throws StorageQueryException, SQLException { + String QUERY = "SELECT * FROM " + Config.getConfig(start).getTotpUserDevicesTable() + + " WHERE user_id = ?;"; + + return execute(start, QUERY, pst -> pst.setString(1, userId), result -> { + List devices = new ArrayList<>(); + while (result.next()) { + devices.add(TOTPDeviceRowMapper.getInstance().map(result)); + } + + return devices.toArray(TOTPDevice[]::new); + }); + } + + public static TOTPDevice[] getDevices_Transaction(Start start, Connection con, String userId) + throws StorageQueryException, SQLException { + String QUERY = "SELECT * FROM " + Config.getConfig(start).getTotpUserDevicesTable() + + " WHERE user_id = ? FOR UPDATE;"; + + return execute(con, QUERY, pst -> pst.setString(1, userId), result -> { + List devices = new ArrayList<>(); + while (result.next()) { + devices.add(TOTPDeviceRowMapper.getInstance().map(result)); + } + + return devices.toArray(TOTPDevice[]::new); + }); + + } + + public static int insertUsedCode_Transaction(Start start, Connection con, TOTPUsedCode code) + throws SQLException, StorageQueryException { + String QUERY = "INSERT INTO " + Config.getConfig(start).getTotpUsedCodesTable() + + " (user_id, code, is_valid, expiry_time_ms, created_time_ms) VALUES (?, ?, ?, ?, ?);"; + + return update(con, QUERY, pst -> { + pst.setString(1, code.userId); + pst.setString(2, code.code); + pst.setBoolean(3, code.isValid); + pst.setLong(4, code.expiryTime); + pst.setLong(5, code.createdTime); + }); + } + + /** + * Query to get all used codes (expired/non-expired) for a user in descending + * order of creation time. + */ + public static TOTPUsedCode[] getAllUsedCodesDescOrder_Transaction(Start start, Connection con, + String userId) + throws SQLException, StorageQueryException { + // Take a lock based on the user id: + String QUERY = "SELECT * FROM " + + Config.getConfig(start).getTotpUsedCodesTable() + + " WHERE user_id = ? ORDER BY created_time_ms DESC FOR UPDATE;"; + return execute(con, QUERY, pst -> { + pst.setString(1, userId); + }, result -> { + List codes = new ArrayList<>(); + while (result.next()) { + codes.add(TOTPUsedCodeRowMapper.getInstance().map(result)); + } + + return codes.toArray(TOTPUsedCode[]::new); + }); + } + + public static int removeExpiredCodes(Start start, long expiredBefore) + throws StorageQueryException, SQLException { + String QUERY = "DELETE FROM " + Config.getConfig(start).getTotpUsedCodesTable() + + " WHERE expiry_time_ms < ?;"; + + return update(start, QUERY, pst -> pst.setLong(1, expiredBefore)); + } + + private static class TOTPDeviceRowMapper implements RowMapper { + private static final TOTPDeviceRowMapper INSTANCE = new TOTPDeviceRowMapper(); + + private TOTPDeviceRowMapper() { + } + + private static TOTPDeviceRowMapper getInstance() { + return INSTANCE; + } + + @Override + public TOTPDevice map(ResultSet result) throws SQLException { + return new TOTPDevice( + result.getString("user_id"), + result.getString("device_name"), + result.getString("secret_key"), + result.getInt("period"), + result.getInt("skew"), + result.getBoolean("verified")); + } + } + + private static class TOTPUsedCodeRowMapper implements RowMapper { + private static final TOTPUsedCodeRowMapper INSTANCE = new TOTPUsedCodeRowMapper(); + + private TOTPUsedCodeRowMapper() { + } + + private static TOTPUsedCodeRowMapper getInstance() { + return INSTANCE; + } + + @Override + public TOTPUsedCode map(ResultSet result) throws SQLException { + return new TOTPUsedCode( + result.getString("user_id"), + result.getString("code"), + result.getBoolean("is_valid"), + result.getLong("expiry_time_ms"), + result.getLong("created_time_ms")); + } + } +} diff --git a/src/test/java/io/supertokens/storage/postgresql/test/DeadlockTest.java b/src/test/java/io/supertokens/storage/postgresql/test/DeadlockTest.java index 71a4b17f..b094e87c 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/DeadlockTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/DeadlockTest.java @@ -23,9 +23,16 @@ import io.supertokens.pluginInterface.Storage; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.sqlStorage.SQLStorage; +import io.supertokens.pluginInterface.sqlStorage.SQLStorage.TransactionIsolationLevel; +import io.supertokens.pluginInterface.totp.TOTPDevice; +import io.supertokens.pluginInterface.totp.TOTPUsedCode; +import io.supertokens.pluginInterface.totp.exception.TotpNotEnabledException; +import io.supertokens.pluginInterface.totp.exception.UsedCodeAlreadyExistsException; +import io.supertokens.pluginInterface.totp.sqlStorage.TOTPSQLStorage; import io.supertokens.storageLayer.StorageLayer; import org.junit.AfterClass; import org.junit.Before; @@ -33,13 +40,17 @@ import org.junit.Test; import org.junit.rules.TestRule; +import java.sql.Connection; +import java.sql.SQLException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; -import static org.junit.Assert.*; +import static io.supertokens.storage.postgresql.QueryExecutorTemplate.update; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; public class DeadlockTest { @Rule @@ -55,6 +66,9 @@ public void beforeEach() { Utils.reset(); } + @Rule + public Retry retry = new Retry(3); + @Test public void transactionDeadlockTesting() throws InterruptedException, StorageQueryException, StorageTransactionLogicException { @@ -226,13 +240,337 @@ public void testCodeCreationRapidlyWithDifferentEmails() throws Exception { es.shutdown(); es.awaitTermination(2, TimeUnit.MINUTES); - assertNull(process.checkOrWaitForEventInPlugin( - io.supertokens.storage.postgresql.ProcessState.PROCESS_STATE.DEADLOCK_FOUND)); assert (pass.get()); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } + + @Test + public void testConcurrentDeleteAndUpdate() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + Storage storage = StorageLayer.getStorage(process.getProcess()); + SQLStorage sqlStorage = (SQLStorage) storage; + + // Create a device as well as a user: + TOTPSQLStorage totpStorage = (TOTPSQLStorage) StorageLayer.getStorage(process.getProcess()); + TOTPDevice device = new TOTPDevice("user", "d1", "secret", 30, 1, false); + totpStorage.createDevice(new AppIdentifier(null, null), device); + + long now = System.currentTimeMillis(); + long nextDay = now + 1000 * 60 * 60 * 24; // 1 day from now + TOTPUsedCode code = new TOTPUsedCode("user", "1234", true, nextDay, now); + totpStorage.startTransaction(con -> { + try { + totpStorage.insertUsedCode_Transaction(con, new TenantIdentifier(null, null, null), code); + totpStorage.commitTransaction(con); + } catch (TotpNotEnabledException | UsedCodeAlreadyExistsException e) { + // This should not happen + throw new StorageTransactionLogicException(e); + } + return null; + }); + + final Object syncObject = new Object(); + + AtomicReference t1State = new AtomicReference<>("init"); + AtomicReference t2State = new AtomicReference<>("init"); + + AtomicBoolean t1Failed = new AtomicBoolean(true); + AtomicBoolean t2Failed = new AtomicBoolean(true); + + Runnable r1 = () -> { + try { + sqlStorage.startTransaction(con -> { + // Isolation level is SERIALIZABLE + Connection sqlCon = (Connection) con.getConnection(); + + String QUERY = "DELETE FROM totp_users where user_id = ?"; + try { + update(sqlCon, QUERY, pst -> { + pst.setString(1, "user"); + }); + } catch (SQLException e) { + // Something is wrong with the test + // This should not happen + throw new StorageTransactionLogicException(e); + } + // Removal of user also triggers removal of the devices because + // of FOREIGN KEY constraint. + + synchronized (syncObject) { + // Notify t2 that that device has been deleted by t1 + t1State.set("query"); + syncObject.notifyAll(); + } + + // Wait for t2 to run the update the device query before committing + synchronized (syncObject) { + + while (t2State.get().equals("init")) { + try { + syncObject.wait(); + } catch (InterruptedException ignored) { + } + } + } + + sqlStorage.commitTransaction(con); + t1State.set("commit"); + t1Failed.set(false); + + return null; + }); + } catch (StorageQueryException | StorageTransactionLogicException e) { + // This is expected because of "could not serialize access" + t1Failed.set(true); + } + }; + + Runnable r2 = () -> { + try { + sqlStorage.startTransaction(con -> { + // Isolation level is SERIALIZABLE + Connection sqlCon = (Connection) con.getConnection(); + + synchronized (syncObject) { + // Wait for t1 to run delete device query first + while (t1State.get().equals("init")) { + try { + syncObject.wait(); + } catch (InterruptedException ignored) { + } + } + } + + Runnable r2Inner = () -> { + // Wait for t2Inner to start running + try { + Thread.sleep(1500); + } catch (InterruptedException ignored) { + } + + // The psql txn will wait block t2Inner thread + // but the t2 txn is still free and can commit. + + synchronized (syncObject) { + // Notify t1 that that device has been updated by t2 + t2State.set("query"); + syncObject.notifyAll(); + } + }; + + Thread t2Inner = new Thread(r2Inner); + t2Inner.start(); + // We will not wait for t2Inner to finish + + String QUERY = "UPDATE totp_used_codes SET is_valid=false WHERE user_id = ?"; + try { + update(sqlCon, QUERY, pst -> { + pst.setString(1, "user"); + }); + } catch (SQLException e) { + throw new StorageTransactionLogicException(e); + } + sqlStorage.commitTransaction(con); + t2State.set("commit"); + t2Failed.set(false); + + return null; + }); + } catch (StorageQueryException | StorageTransactionLogicException e) { + t2Failed.set(true); + } + }; + + Thread t1 = new Thread(r2); + Thread t2 = new Thread(r1); + + t1.start(); + t2.start(); + + t1.join(); + t2.join(); + + // t1 (delete) should succeed + // but t2 (update) should fail because of "could not serialize access" + assertTrue(!t1Failed.get() && !t2Failed.get()); + assert (t1State.get().equals("commit") && t2State.get().equals("commit")); + assertNotNull(process.checkOrWaitForEventInPlugin( + io.supertokens.storage.postgresql.ProcessState.PROCESS_STATE.DEADLOCK_FOUND)); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testConcurrentDeleteAndInsert() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + Storage storage = StorageLayer.getStorage(process.getProcess()); + SQLStorage sqlStorage = (SQLStorage) storage; + + // Create a device as well as a user: + TOTPSQLStorage totpStorage = (TOTPSQLStorage) StorageLayer.getStorage(process.getProcess()); + TOTPDevice device = new TOTPDevice("user", "d1", "secret", 30, 1, false); + totpStorage.createDevice(new AppIdentifier(null, null), device); + + long now = System.currentTimeMillis(); + long nextDay = now + 1000 * 60 * 60 * 24; // 1 day from now + TOTPUsedCode code = new TOTPUsedCode("user", "1234", true, nextDay, now); + totpStorage.startTransaction(con -> { + try { + totpStorage.insertUsedCode_Transaction(con, new TenantIdentifier(null, null, null), code); + totpStorage.commitTransaction(con); + } catch (TotpNotEnabledException | UsedCodeAlreadyExistsException e) { + // This should not happen + throw new StorageTransactionLogicException(e); + } + return null; + }); + + final Object syncObject = new Object(); + + AtomicReference t1State = new AtomicReference<>("init"); + AtomicReference t2State = new AtomicReference<>("init"); + + AtomicBoolean t1Failed = new AtomicBoolean(true); + AtomicBoolean t2Failed = new AtomicBoolean(false); + + Runnable r1 = () -> { + try { + sqlStorage.startTransaction(con -> { + // Isolation level is SERIALIZABLE + Connection sqlCon = (Connection) con.getConnection(); + + String QUERY = "DELETE FROM totp_users where user_id = ?"; + try { + update(sqlCon, QUERY, pst -> { + pst.setString(1, "user"); + }); + } catch (SQLException e) { + // Something is wrong with the test + // This should not happen + throw new StorageTransactionLogicException(e); + } + // Removal of user also triggers removal of the devices because + // of FOREIGN KEY constraint. + + synchronized (syncObject) { + // Notify t2 that that device has been deleted by t1 + t1State.set("query"); + syncObject.notifyAll(); + } + + // Wait for t2 to run the update the device query before committing + synchronized (syncObject) { + + while (t2State.get().equals("init")) { + try { + syncObject.wait(); + } catch (InterruptedException ignored) { + } + } + } + + sqlStorage.commitTransaction(con); + t1State.set("commit"); + t1Failed.set(false); + + return null; + }, TransactionIsolationLevel.SERIALIZABLE); + } catch (StorageQueryException | StorageTransactionLogicException e) { + // This is expected because of "could not serialize access" + t1Failed.set(true); + } + }; + + Runnable r2 = () -> { + try { + sqlStorage.startTransaction(con -> { + // Isolation level is SERIALIZABLE + synchronized (syncObject) { + // Wait for t1 to run delete device query first + while (t1State.get().equals("init")) { + try { + syncObject.wait(); + } catch (InterruptedException ignored) { + } + } + } + + Runnable r2Inner = () -> { + // Wait for t2Inner to start running + try { + Thread.sleep(1500); + } catch (InterruptedException ignored) { + } + + // The psql txn will wait block t2Inner thread + // but the t2 txn is still free and can commit. + + synchronized (syncObject) { + // Notify t1 that that device has been updated by t2 + t2State.set("query"); + syncObject.notifyAll(); + } + }; + + Thread t2Inner = new Thread(r2Inner); + t2Inner.start(); + // We will not wait for t2Inner to finish + + TOTPUsedCode code2 = new TOTPUsedCode("user", "1234", false, nextDay, now + 1); + try { + totpStorage.insertUsedCode_Transaction(con, new TenantIdentifier(null, null, null), code2); + } catch (TotpNotEnabledException | UsedCodeAlreadyExistsException e) { + // This should not happen + throw new StorageTransactionLogicException(e); + } + sqlStorage.commitTransaction(con); + t2State.set("commit"); + t2Failed.set(false); + + return null; + }); + } catch (StorageTransactionLogicException e) { + Exception e2 = e.actualException; + + if (e2 instanceof TotpNotEnabledException) { + t2Failed.set(true); + } + } catch (StorageQueryException e) { + t2Failed.set(false); + } + }; + + Thread t1 = new Thread(r2); + Thread t2 = new Thread(r1); + + t1.start(); + t2.start(); + + t1.join(); + t2.join(); + + // t1 (delete) should succeed + // but t2 (insert) should fail because of "could not serialize access" + assertTrue(!t1Failed.get() && t2Failed.get()); + assert (t1State.get().equals("commit") && t2State.get().equals("query")); + assertNotNull(process + .checkOrWaitForEventInPlugin( + io.supertokens.storage.postgresql.ProcessState.PROCESS_STATE.DEADLOCK_FOUND, + 1000)); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + } /* @@ -245,16 +583,22 @@ public void testCodeCreationRapidlyWithDifferentEmails() throws Exception { * TRANSACTION 196679, ACTIVE 0 sec starting index read * mysql tables in use 1, locked 1 * LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s) - * MySQL thread id 15926, OS thread handle 140000503174912, query id 335354 172.31.0.253 executionMaster statistics - * SELECT value, created_at_time FROM key_value WHERE name = 'access_token_signing_key' FOR UPDATE + * MySQL thread id 15926, OS thread handle 140000503174912, query id 335354 + * 172.31.0.253 executionMaster statistics + * SELECT value, created_at_time FROM key_value WHERE name = + * 'access_token_signing_key' FOR UPDATE *** (1) WAITING FOR THIS LOCK TO BE GRANTED: - * RECORD LOCKS space id 61 page no 3 n bits 80 index PRIMARY of table `supertokens`.`key_value` trx id 196679 lock_mode + * RECORD LOCKS space id 61 page no 3 n bits 80 index PRIMARY of table + * `supertokens`.`key_value` trx id 196679 lock_mode * X locks rec but not gap waiting - * Record lock, heap no 8 PHYSICAL RECORD: n_fields 5; compact format; info bits 0 - * 0: len 24; hex 6163636573735f746f6b656e5f7369676e696e675f6b6579; asc access_token_signing_key;; + * Record lock, heap no 8 PHYSICAL RECORD: n_fields 5; compact format; info bits + * 0 + * 0: len 24; hex 6163636573735f746f6b656e5f7369676e696e675f6b6579; asc + * access_token_signing_key;; * 1: len 6; hex 000000030003; asc ;; * 2: len 7; hex 39000001dd08b4; asc 9 ;; - * 3: len 30; hex 4d494942496a414e42676b71686b6947397730424151454641414f434151; asc MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ; + * 3: len 30; hex 4d494942496a414e42676b71686b6947397730424151454641414f434151; + * asc MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ; * (total 2017 bytes); * 4: len 8; hex 0000016fa2a3229a; asc o " ;; *** @@ -262,39 +606,53 @@ public void testCodeCreationRapidlyWithDifferentEmails() throws Exception { * TRANSACTION 196680, ACTIVE 0 sec inserting * mysql tables in use 1, locked 1 * 3 lock struct(s), heap size 1136, 2 row lock(s) - * MySQL thread id 15927, OS thread handle 140000503441152, query id 335358 172.31.0.253 executionMaster update - * INSERT INTO key_value(name, value, created_at_time) VALUES('access_token_signing_key', + * MySQL thread id 15927, OS thread handle 140000503441152, query id 335358 + * 172.31.0.253 executionMaster update + * INSERT INTO key_value(name, value, created_at_time) + * VALUES('access_token_signing_key', * 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApm557QfYLxLc6HmqBMnd3Uz5mKyXpgZr0li1YkIZf8MfIbcVl7l7qlffZmjhgtkIGGVi1yXNFyItM - * +2N2sOsF9c4qks3BoIkrW0ACltcmqc3wxGEQMfsPYsxRuRMlWnC0nZCzO5MEyVcV7JciSBKc00HzwNrHXsC231Qlh5cJo5/Yun/ - * faW715MaHwLCrvAKXF2/yI2BFAtSBcsgVTv/ZNPuEbadPdg5utN3qSHOmK/hsrQIpZYVhghNFm0q1f90D4cOtFYpJbtUAaHJ+ + * +2N2sOsF9c4qks3BoIkrW0ACltcmqc3wxGEQMfsPYsxRuRMlWnC0nZCzO5MEyVcV7JciSBKc00HzwNrHXsC231Qlh5cJo5 + * /Yun/ + * faW715MaHwLCrvAKXF2/yI2BFAtSBcsgVTv/ZNPuEbadPdg5utN3qSHOmK/ + * hsrQIpZYVhghNFm0q1f90D4cOtFYpJbtUAaHJ+ * D46kh6RDk1ua6XunpUpbnGhEwtFa8BuEKq+Au5YWcxddxb/xE7h7oIzzE0SCao01ANlFwIDAQAB; * MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCmbnntB9gvEtzoeaoEyd3dTPmYrJemBmvSWLViQhl/ * wx8htxWXuXuqV99maOGC2QgYZWLXJc0XIi0z7Y3aw6wX1ziqSzcGgiStbQAKW1yapzfDEYRAx+ - * w9izFG5EyVacLSdkLM7kwTJVxXslyJIEpzTQfPA2sdewLbfVCWHlwmjn9i6f99pbvXkxofAsKu8ApcXb/IjYEUC1IFyyBVO/9k0+ - * 4Rtp092Dm603epIc6Yr+GytAillhWGCE0WbSrV/3QPhw60Viklu1QBocn4PjqSHpEOTW5rpe6elSlucaETC0VrwG4Qqr4C7lhZzF13Fv/ + * w9izFG5EyVacLSdkLM7kwTJVxXslyJIEpzTQfPA2sdewLbfVCWHlwmjn9i6f99pbvXkxofAsKu8ApcXb + * /IjYEUC1IFyyBVO/9k0+ + * 4Rtp092Dm603epIc6Yr+GytAillhWGCE0WbSrV/ + * 3QPhw60Viklu1QBocn4PjqSHpEOTW5rpe6elSlucaETC0VrwG4Qqr4C7lhZzF13Fv/ * ETuHugjPMTRIJqjTUA2UXAgMBAAECggEBAJD8RPMcllPL1u4eruIlCUY0PGuoT *** (2) HOLDS THE LOCK(S): - * RECORD LOCKS space id 61 page no 3 n bits 80 index PRIMARY of table `supertokens`.`key_value` trx id 196680 lock_mode + * RECORD LOCKS space id 61 page no 3 n bits 80 index PRIMARY of table + * `supertokens`.`key_value` trx id 196680 lock_mode * X locks rec but not gap - * Record lock, heap no 8 PHYSICAL RECORD: n_fields 5; compact format; info bits 0 - * 0: len 24; hex 6163636573735f746f6b656e5f7369676e696e675f6b6579; asc access_token_signing_key;; + * Record lock, heap no 8 PHYSICAL RECORD: n_fields 5; compact format; info bits + * 0 + * 0: len 24; hex 6163636573735f746f6b656e5f7369676e696e675f6b6579; asc + * access_token_signing_key;; * 1: len 6; hex 000000030003; asc ;; * 2: len 7; hex 39000001dd08b4; asc 9 ;; - * 3: len 30; hex 4d494942496a414e42676b71686b6947397730424151454641414f434151; asc MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ; + * 3: len 30; hex 4d494942496a414e42676b71686b6947397730424151454641414f434151; + * asc MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ; * (total 2017 bytes); * 4: len 8; hex 0000016fa2a3229a; asc o " ;; *** * (2) WAITING FOR THIS LOCK TO BE GRANTED: - * RECORD LOCKS space id 61 page no 3 n bits 80 index PRIMARY of table `supertokens`.`key_value` trx id 196680 lock_mode + * RECORD LOCKS space id 61 page no 3 n bits 80 index PRIMARY of table + * `supertokens`.`key_value` trx id 196680 lock_mode * X waiting - * Record lock, heap no 8 PHYSICAL RECORD: n_fields 5; compact format; info bits 0 - * 0: len 24; hex 6163636573735f746f6b656e5f7369676e696e675f6b6579; asc access_token_signing_key;; + * Record lock, heap no 8 PHYSICAL RECORD: n_fields 5; compact format; info bits + * 0 + * 0: len 24; hex 6163636573735f746f6b656e5f7369676e696e675f6b6579; asc + * access_token_signing_key;; * 1: len 6; hex 000000030003; asc ;; * 2: len 7; hex 39000001dd08b4; asc 9 ;; - * 3: len 30; hex 4d494942496a414e42676b71686b6947397730424151454641414f434151; asc MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ; + * 3: len 30; hex 4d494942496a414e42676b71686b6947397730424151454641414f434151; + * asc MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ; * (total 2017 bytes); * 4: len 8; hex 0000016fa2a3229a; asc o " ;; *** * WE ROLL BACK TRANSACTION (1) * - */ \ No newline at end of file + */ diff --git a/src/test/java/io/supertokens/storage/postgresql/test/Retry.java b/src/test/java/io/supertokens/storage/postgresql/test/Retry.java new file mode 100644 index 00000000..b9464297 --- /dev/null +++ b/src/test/java/io/supertokens/storage/postgresql/test/Retry.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2022, 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.storage.postgresql.test; + +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +public class Retry implements TestRule { + private final int retryCount; + + public Retry(int retryCount) { + this.retryCount = retryCount; + } + + private Statement statement(final Statement base, final Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + Throwable caughtThrowable = null; + + // implement retry logic here + for (int i = 0; i < retryCount; i++) { + try { + base.evaluate(); + return; + } catch (Throwable t) { + caughtThrowable = t; + System.err.println(description.getDisplayName() + ": run " + (i + 1) + " failed"); + } + } + System.err.println(description.getDisplayName() + ": giving up after " + retryCount + " failures"); + throw caughtThrowable; + } + }; + } + + @Override + public Statement apply(Statement base, Description description) { + return statement(base, description); + } +} \ No newline at end of file diff --git a/src/test/java/io/supertokens/storage/postgresql/test/StorageLayerTest.java b/src/test/java/io/supertokens/storage/postgresql/test/StorageLayerTest.java new file mode 100644 index 00000000..0423fcc2 --- /dev/null +++ b/src/test/java/io/supertokens/storage/postgresql/test/StorageLayerTest.java @@ -0,0 +1,93 @@ +package io.supertokens.storage.postgresql.test; + +import io.supertokens.ProcessState; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.totp.TOTPDevice; +import io.supertokens.pluginInterface.totp.TOTPUsedCode; +import io.supertokens.pluginInterface.totp.exception.TotpNotEnabledException; +import io.supertokens.pluginInterface.totp.exception.UsedCodeAlreadyExistsException; +import io.supertokens.pluginInterface.totp.sqlStorage.TOTPSQLStorage; +import io.supertokens.storageLayer.StorageLayer; +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.assertNotNull; + +public class StorageLayerTest { + + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + // TOTP recipe: + + public static void insertUsedCodeUtil(TOTPSQLStorage storage, TOTPUsedCode usedCode) throws Exception { + try { + storage.startTransaction(con -> { + try { + storage.insertUsedCode_Transaction(con, new TenantIdentifier(null, null, null), usedCode); + storage.commitTransaction(con); + return null; + } catch (TotpNotEnabledException | UsedCodeAlreadyExistsException e) { + throw new StorageTransactionLogicException(e); + } + }); + } catch (StorageTransactionLogicException e) { + Exception actual = e.actualException; + if (actual instanceof TotpNotEnabledException || actual instanceof UsedCodeAlreadyExistsException) { + throw actual; + } else { + throw e; + } + } + } + + @Test + public void totpCodeLengthTest() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + TOTPSQLStorage storage = (TOTPSQLStorage) StorageLayer.getStorage(process.getProcess()); + long now = System.currentTimeMillis(); + long nextDay = now + 1000 * 60 * 60 * 24; // 1 day from now + + TOTPDevice d1 = new TOTPDevice("user", "d1", "secret", 30, 1, false); + storage.createDevice(new AppIdentifier(null, null), d1); + + // Try code with length > 8 + try { + TOTPUsedCode code = new TOTPUsedCode("user", "123456789", true, nextDay, now); + insertUsedCodeUtil(storage, code); + assert (false); + } catch (StorageQueryException e) { + assert e.getMessage().endsWith("ERROR: value too long for type character varying(8)"); + } + + // Try code with length < 8 + TOTPUsedCode code = new TOTPUsedCode("user", "12345678", true, nextDay, now); + insertUsedCodeUtil(storage, code); + } + +} diff --git a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java index b3a81be1..e616e297 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java @@ -19,13 +19,14 @@ import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import io.supertokens.ProcessState; +import io.supertokens.ResourceDistributor; import io.supertokens.config.Config; 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.exceptions.DbInitException; import io.supertokens.pluginInterface.exceptions.InvalidConfigException; -import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.*; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.storage.postgresql.Start; @@ -34,14 +35,14 @@ import io.supertokens.storageLayer.StorageLayer; import org.junit.*; import org.junit.rules.TestRule; -import org.postgresql.util.PSQLException; import java.io.IOException; -import java.sql.SQLTransientConnectionException; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; import static org.junit.Assert.*; @@ -721,4 +722,58 @@ public void multipleTenantsSameUserPoolAndDifferentConnectionPoolShouldWork() process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } + + @Test + public void testCreating50StorageLayersUsage() + throws InterruptedException { + 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)); + + TenantConfig[] tenants = new TenantConfig[1000]; + + ExecutorService executor = Executors.newFixedThreadPool(50); + for (int i = 0; i < 50; i++) { + final int insideLoop = i; + executor.submit(() -> { + JsonObject config = new JsonObject(); + config.addProperty("postgresql_database_name", "st" + insideLoop); + tenants[insideLoop] = new TenantConfig(new TenantIdentifier(null, "a" + insideLoop, null), + new EmailPasswordConfig(false), + new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), + new PasswordlessConfig(false), + config); + try { + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantIdentifier(null, null, null), + tenants[insideLoop]); + } catch (Exception e) { + e.printStackTrace(); + } + }); + } + + executor.shutdown(); + executor.awaitTermination(5, TimeUnit.MINUTES); + + Map map = process.getProcess() + .getResourceDistributor().getAllResourcesWithResourceKey(StorageLayer.RESOURCE_KEY); + Set uniqueResources = new HashSet<>(); + for (ResourceDistributor.SingletonResource resource : map.values()) { + StorageLayer storage = (StorageLayer) resource; + if (uniqueResources.contains(storage.getUnderlyingStorage())) { + continue; + } + uniqueResources.add(storage.getUnderlyingStorage()); + } + assertEquals(uniqueResources.size(), 51); + + // TODO: we need to test recipe usage for the apps + RAM usage. + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } } diff --git a/startDb.sh b/startDb.sh index e7000cac..5fa7d75e 100755 --- a/startDb.sh +++ b/startDb.sh @@ -1 +1 @@ -docker run --rm --name postgres -e 'POSTGRES_USER=root' -e 'POSTGRES_PASSWORD=root' -d -p 5432:5432 -v ~/Desktop/db/pstgres:/var/lib/postgresql/data postgres \ No newline at end of file +docker run --rm --name postgres -e 'POSTGRES_USER=root' -e 'POSTGRES_PASSWORD=root' -d -p 5432:5432 -v ~/Desktop/db/pstgres:/var/lib/postgresql/data postgres -c 'max_connections=10000' \ No newline at end of file From ac16c99ea29ad0f5f3a5d2942967ec7af9f881d0 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Thu, 6 Apr 2023 18:51:50 +0530 Subject: [PATCH 055/106] many fixes --- .../storage/postgresql/ConnectionPool.java | 41 ++- .../supertokens/storage/postgresql/Start.java | 20 +- .../postgresql/queries/GeneralQueries.java | 14 +- .../storage/postgresql/test/ConfigTest.java | 25 +- .../test/TestingProcessManager.java | 8 +- .../test/multitenancy/StorageLayerTest.java | 246 +++++++++++++----- 6 files changed, 254 insertions(+), 100 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/ConnectionPool.java b/src/main/java/io/supertokens/storage/postgresql/ConnectionPool.java index 0391d503..51c0cad9 100644 --- a/src/main/java/io/supertokens/storage/postgresql/ConnectionPool.java +++ b/src/main/java/io/supertokens/storage/postgresql/ConnectionPool.java @@ -33,9 +33,17 @@ public class ConnectionPool extends ResourceDistributor.SingletonResource { private static final String RESOURCE_KEY = "io.supertokens.storage.postgresql.ConnectionPool"; - private final HikariDataSource hikariDataSource; + private HikariDataSource hikariDataSource; + private final Start start; private ConnectionPool(Start start) { + this.start = start; + } + + private synchronized void initialiseHikariDataSource() throws SQLException { + if (this.hikariDataSource != null) { + return; + } if (!start.enabled) { throw new RuntimeException("Connection to refused"); // emulates exception thrown by Hikari } @@ -82,7 +90,11 @@ private ConnectionPool(Start start) { // - Failed to validate connection org.mariadb.jdbc.MariaDbConnection@79af83ae (Connection.setNetworkTimeout // cannot be called on a closed connection). Possibly consider using a shorter maxLifetime value. config.setPoolName(start.getUserPoolId() + "~" + start.getConnectionPoolId()); - hikariDataSource = new HikariDataSource(config); + try { + hikariDataSource = new HikariDataSource(config); + } catch (Exception e) { + throw new SQLException(e); + } } private static int getTimeToWaitToInit(Start start) { @@ -110,10 +122,10 @@ private static ConnectionPool getInstance(Start start) { } static boolean isAlreadyInitialised(Start start) { - return getInstance(start) != null; + return getInstance(start) != null && getInstance(start).hikariDataSource != null; } - static void initPool(Start start) throws DbInitException { + static void initPool(Start start, boolean shouldWait) throws DbInitException { if (isAlreadyInitialised(start)) { return; } @@ -129,11 +141,16 @@ static void initPool(Start start) throws DbInitException { " specified the correct values for ('postgresql_host' and 'postgresql_port') or for " + "'postgresql_connection_uri'"; try { + ConnectionPool con = new ConnectionPool(start); + start.getResourceDistributor().setResource(RESOURCE_KEY, con); while (true) { try { - start.getResourceDistributor().setResource(RESOURCE_KEY, new ConnectionPool(start)); + con.initialiseHikariDataSource(); break; } catch (Exception e) { + if (!shouldWait) { + throw new DbInitException(e); + } if (e.getMessage().contains("Connection to") && e.getMessage().contains("refused") || e.getMessage().contains("the database system is starting up")) { start.handleKillSignalForWhenItHappens(); @@ -158,7 +175,7 @@ static void initPool(Start start) throws DbInitException { throw new DbInitException(errorMessage); } } else { - throw e; + throw new DbInitException(e); } } } @@ -174,6 +191,9 @@ public static Connection getConnection(Start start) throws SQLException { if (!start.enabled) { throw new SQLException("Storage layer disabled"); } + if (getInstance(start).hikariDataSource == null) { + getInstance(start).initialiseHikariDataSource(); + } return getInstance(start).hikariDataSource.getConnection(); } @@ -181,6 +201,13 @@ static void close(Start start) { if (getInstance(start) == null) { return; } - getInstance(start).hikariDataSource.close(); + if (getInstance(start).hikariDataSource != null) { + try { + getInstance(start).hikariDataSource.close(); + } finally { + // we mark it as null so that next time it's being initialised, it will be initialised again + getInstance(start).hikariDataSource = null; + } + } } } diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 73377e51..ab085dbf 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -20,6 +20,7 @@ import ch.qos.logback.classic.Logger; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; +import com.zaxxer.hikari.pool.HikariPool; import io.supertokens.pluginInterface.*; import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.dashboard.DashboardSearchTags; @@ -201,12 +202,12 @@ public void stopLogging() { } @Override - public void initStorage() throws DbInitException { + public void initStorage(boolean shouldWait) throws DbInitException { if (ConnectionPool.isAlreadyInitialised(this)) { return; } try { - ConnectionPool.initPool(this); + ConnectionPool.initPool(this, shouldWait); GeneralQueries.createTablesIfNotExists(this); } catch (Exception e) { throw new DbInitException(e); @@ -261,9 +262,9 @@ public T startTransaction(TransactionLogic logic, TransactionIsolationLev // we have deadlock as well due to the DeadlockTest.java exceptionMessage.toLowerCase().contains("deadlock"); - if ((isPSQLRollbackException || isDeadlockException) && tries < 50) { + if ((isPSQLRollbackException || isDeadlockException) && tries < 20) { try { - Thread.sleep((long) (10 + (Math.random() * 20))); + Thread.sleep((long) (10 + Math.min(tries, 10) * (Math.random() * 20))); } catch (InterruptedException ignored) { } ProcessState.getInstance(this).addState(ProcessState.PROCESS_STATE.DEADLOCK_FOUND, e); @@ -379,7 +380,8 @@ public void addAccessTokenSigningKey_Transaction(AppIdentifier appIdentifier, Tr throws StorageQueryException, TenantOrAppNotFoundException { Connection sqlCon = (Connection) con.getConnection(); try { - SessionQueries.addAccessTokenSigningKey_Transaction(this, sqlCon, appIdentifier, info.createdAtTime, info.value); + SessionQueries.addAccessTokenSigningKey_Transaction(this, sqlCon, appIdentifier, info.createdAtTime, + info.value); } catch (SQLException e) { if (e instanceof PSQLException) { PostgreSQLConfig config = Config.getConfig(this); @@ -435,7 +437,13 @@ public void deleteAllInformation() throws StorageQueryException { try { GeneralQueries.deleteAllTables(this); } catch (SQLException e) { - throw new StorageQueryException(e); + if (e.getCause() instanceof HikariPool.PoolInitializationException) { + // this can happen if the db being connected to is not actually present. + // So we ignore this since there are tests in which we are adding a non existent db for a tenant, + // and we want to not throw errors in the next test wherein this function is called. + } else { + throw new StorageQueryException(e); + } } } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java index 6cdc6f81..92ac5566 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java @@ -137,7 +137,7 @@ private static String getQueryToCreateKeyValueTable(Start start) { + " PRIMARY KEY(app_id, tenant_id, name)," + "CONSTRAINT " + Utils.getConstraintName(schema, keyValueTable, "tenant_id", "fkey") + " FOREIGN KEY(app_id, tenant_id)" - + " REFERENCES " + Config.getConfig(start).getTenantsTable() + " (app_id, tenant_id) ON DELETE CASCADE" + + " REFERENCES " + Config.getConfig(start).getTenantsTable() + " (app_id, tenant_id) ON DELETE CASCADE" + ");"; // @formatter:on } @@ -435,7 +435,8 @@ public static void deleteAllTables(Start start) throws SQLException, StorageQuer } } - public static void setKeyValue_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, String key, KeyValueInfo info) + public static void setKeyValue_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, + String key, KeyValueInfo info) throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + getConfig(start).getKeyValueTable() + "(app_id, tenant_id, name, value, created_at_time) VALUES(?, ?, ?, ?, ?) " @@ -459,7 +460,8 @@ public static void setKeyValue(Start start, TenantIdentifier tenantIdentifier, S } } - public static KeyValueInfo getKeyValue(Start start, TenantIdentifier tenantIdentifier, String key) throws SQLException, StorageQueryException { + public static KeyValueInfo getKeyValue(Start start, TenantIdentifier tenantIdentifier, String key) + throws SQLException, StorageQueryException { String QUERY = "SELECT value, created_at_time FROM " + getConfig(start).getKeyValueTable() + " WHERE app_id = ? AND tenant_id = ? AND name = ?"; @@ -475,7 +477,8 @@ public static KeyValueInfo getKeyValue(Start start, TenantIdentifier tenantIdent }); } - public static KeyValueInfo getKeyValue_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, String key) + public static KeyValueInfo getKeyValue_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, + String key) throws SQLException, StorageQueryException { String QUERY = "SELECT value, created_at_time FROM " + getConfig(start).getKeyValueTable() + " WHERE app_id = ? AND tenant_id = ? AND name = ? FOR UPDATE"; @@ -492,7 +495,8 @@ public static KeyValueInfo getKeyValue_Transaction(Start start, Connection con, }); } - public static void deleteKeyValue_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, String key) + public static void deleteKeyValue_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, + String key) throws SQLException, StorageQueryException { String QUERY = "DELETE FROM " + getConfig(start).getKeyValueTable() + " WHERE app_id = ? AND tenant_id = ? AND name = ?"; diff --git a/src/test/java/io/supertokens/storage/postgresql/test/ConfigTest.java b/src/test/java/io/supertokens/storage/postgresql/test/ConfigTest.java index b6971754..e25afc18 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/ConfigTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/ConfigTest.java @@ -88,6 +88,8 @@ public void testThatCustomConfigLoadsCorrectly() throws Exception { assertEquals(config.getConnectionPoolSize(), 5); assertEquals(config.getKeyValueTable(), "temp_name"); + process.getProcess().deleteAllInformationForTesting(); + process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } @@ -218,7 +220,9 @@ public void testBadHostInput() throws Exception { ProcessState.EventAndException e = process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.INIT_FAILURE); assertNotNull(e); - assertEquals("Failed to initialize pool: The connection attempt failed.", + assertEquals( + "java.sql.SQLException: com.zaxxer.hikari.pool.HikariPool$PoolInitializationException: Failed to " + + "initialize pool: The connection attempt failed.", e.exception.getCause().getCause().getMessage()); process.kill(); @@ -311,16 +315,17 @@ public void testAddingSchemaWorks() throws Exception { assert sessionInfo.accessToken != null; assert sessionInfo.refreshToken != null; + try { + TestCase.assertEquals(((SessionStorage) StorageLayer.getStorage(process.getProcess())) + .getNumberOfSessions(new TenantIdentifier(null, null, null)), 1); - TestCase.assertEquals(((SessionStorage) StorageLayer.getStorage(process.getProcess())) - .getNumberOfSessions(new TenantIdentifier(null, null, null)), 1); - - process.kill(); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); - - // we call this here so that the database is cleared with the modified table names - // since in postgres, we delete all dbs one by one - TestingProcessManager.deleteAllInformation(); + // we call this here so that the database is cleared with the modified table names + // since in postgres, we delete all dbs one by one + } finally { + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + TestingProcessManager.deleteAllInformation(); + } } @Test diff --git a/src/test/java/io/supertokens/storage/postgresql/test/TestingProcessManager.java b/src/test/java/io/supertokens/storage/postgresql/test/TestingProcessManager.java index a34ff61f..84751b4b 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/TestingProcessManager.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/TestingProcessManager.java @@ -129,14 +129,14 @@ String[] getArgs() { return args; } - public void kill() throws InterruptedException { + public void kill(boolean removeAllInfo) throws InterruptedException { if (killed) { return; } // we check if there are multiple user pool IDs loaded, and if there are, // we clear all the info before killing cause otherwise those extra dbs will retain info // across tests - if (StorageLayer.hasMultipleUserPools(this.main)) { + if (removeAllInfo && StorageLayer.hasMultipleUserPools(this.main)) { try { main.deleteAllInformationForTesting(); } catch (Exception e) { @@ -151,6 +151,10 @@ public void kill() throws InterruptedException { killed = true; } + public void kill() throws InterruptedException { + kill(true); + } + public EventAndException checkOrWaitForEvent(PROCESS_STATE state) throws InterruptedException { return checkOrWaitForEvent(state, 15000); } diff --git a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java index e616e297..2440a370 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java @@ -21,18 +21,30 @@ import io.supertokens.ProcessState; import io.supertokens.ResourceDistributor; import io.supertokens.config.Config; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.emailpassword.exceptions.WrongCredentialsException; import io.supertokens.featureflag.EE_FEATURES; 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.multitenancy.exception.DeletionInProgressException; import io.supertokens.pluginInterface.Storage; import io.supertokens.pluginInterface.exceptions.DbInitException; import io.supertokens.pluginInterface.exceptions.InvalidConfigException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.*; +import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateClientTypeException; +import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateTenantException; +import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateThirdPartyIdException; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.test.TestingProcessManager; import io.supertokens.storage.postgresql.test.Utils; import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.thirdparty.InvalidProviderConfigException; import org.junit.*; import org.junit.rules.TestRule; @@ -83,7 +95,7 @@ public void normalDbConfigErrorContinuesToWork() throws InterruptedException, IO @Test public void mergingTenantWithBaseConfigWorks() - throws InterruptedException, IOException, InvalidConfigException, DbInitException, + throws InterruptedException, IOException, InvalidConfigException, TenantOrAppNotFoundException { String[] args = {"../"}; @@ -132,41 +144,6 @@ public void mergingTenantWithBaseConfigWorks() assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } - @Test - public void creatingTenantWithNoExistingDbThrowsError() - throws InterruptedException, IOException, InvalidConfigException, DbInitException { - 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)); - - JsonObject tenantConfig = new JsonObject(); - tenantConfig.add("postgresql_table_names_prefix", new JsonPrimitive("test")); - tenantConfig.add("postgresql_database_name", new JsonPrimitive("random")); - - try { - TenantConfig[] tenants = new TenantConfig[]{ - new TenantConfig(new TenantIdentifier("abc", null, null), new EmailPasswordConfig(false), - new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), - new PasswordlessConfig(false), - tenantConfig)}; - - Config.loadAllTenantConfig(process.getProcess(), tenants); - - StorageLayer.loadAllTenantStorage(process.getProcess(), tenants); - fail(); - } catch (DbInitException e) { - assertEquals(e.getMessage(), "com.zaxxer.hikari.pool.HikariPool$PoolInitializationException: Failed to " + - "initialize pool: FATAL: database \"random\" does not exist"); - } - - process.kill(); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); - } - @Test public void storageInstanceIsReusedAcrossTenants() throws InterruptedException, IOException, InvalidConfigException, DbInitException, @@ -576,40 +553,6 @@ public void testDifferentWaysToGetConfigBasedOnConnectionURIAndTenantId() assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } - @Test - public void differentUserPoolCreatedBasedOnConnectionUri() - throws InterruptedException, IOException, InvalidConfigException { - 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)); - - JsonObject tenantConfig = new JsonObject(); - tenantConfig.add("postgresql_connection_uri", - new JsonPrimitive("postgresql://root:root@localhost:5432/random")); - - try { - TenantConfig[] tenants = new TenantConfig[]{ - new TenantConfig(new TenantIdentifier("abc", null, null), new EmailPasswordConfig(false), - new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), - new PasswordlessConfig(false), - tenantConfig)}; - Config.loadAllTenantConfig(process.getProcess(), tenants); - - StorageLayer.loadAllTenantStorage(process.getProcess(), tenants); - fail(); - } catch (DbInitException e) { - assertEquals(e.getMessage(), "com.zaxxer.hikari.pool.HikariPool$PoolInitializationException: Failed to " + - "initialize pool: FATAL: database \"random\" does not exist"); - } - - process.kill(); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); - } - @Test public void differentUserPoolCreatedBasedOnSchemaInConnectionUri() throws InterruptedException, IOException, InvalidConfigException, DbInitException, @@ -776,4 +719,167 @@ public void testCreating50StorageLayersUsage() process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } + + @Test + public void testCantCreateTenantWithUnknownDb() + throws InterruptedException, IOException, InvalidConfigException, TenantOrAppNotFoundException, + BadPermissionException, InvalidProviderConfigException, + DeletionInProgressException, FeatureNotEnabledException, + CannotModifyBaseConfigException { + 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)); + + JsonObject tenantConfigJson = new JsonObject(); + tenantConfigJson.add("postgresql_connection_uri", + new JsonPrimitive("postgresql://root:root@localhost:5432/random")); + + TenantConfig tenantConfig = new TenantConfig(new TenantIdentifier("abc", null, null), + new EmailPasswordConfig(false), + new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), + new PasswordlessConfig(false), + tenantConfigJson); + + try { + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantIdentifier(null, null, null), + tenantConfig); + fail(); + } catch (StorageQueryException e) { + assertEquals(e.getMessage(), "java.sql.SQLException: com.zaxxer.hikari.pool" + + ".HikariPool$PoolInitializationException: Failed to initialize pool: FATAL: database \"random\" " + + "does not exist"); + } + + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testTenantCreationAndThenDbDownDbThrowsErrorInRecipesAndDoesntAffectCoreStart() + throws InterruptedException, TenantOrAppNotFoundException, + BadPermissionException, DuplicateThirdPartyIdException, DuplicateClientTypeException, + DuplicateTenantException, StorageQueryException, WrongCredentialsException { + { + 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)); + + JsonObject tenantConfigJson = new JsonObject(); + tenantConfigJson.add("postgresql_connection_uri", + new JsonPrimitive("postgresql://root:root@localhost:5432/random")); + + TenantIdentifier tid = new TenantIdentifier("abc", null, null); + + TenantConfig tenantConfig = new TenantConfig(tid, + new EmailPasswordConfig(true), + new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), + new PasswordlessConfig(false), + tenantConfigJson); + + StorageLayer.getMultitenancyStorage(process.getProcess()).createTenant(tenantConfig); + MultitenancyHelper.getInstance(process.getProcess()).refreshTenantsInCoreIfRequired(true); + + try { + EmailPassword.signIn(tid.withStorage(StorageLayer.getStorage(tid, process.getProcess())), + process.getProcess(), "", ""); + fail(); + } catch (StorageQueryException e) { + assertEquals(e.getMessage(), "java.sql.SQLException: com.zaxxer.hikari.pool" + + ".HikariPool$PoolInitializationException: Failed to initialize pool: FATAL: database " + + "\"random\" " + + "does not exist"); + } + + // we do this again just to check that if this function is called again, it fails again and there is no + // side effect of calling the above function + try { + EmailPassword.signIn(tid.withStorage(StorageLayer.getStorage(tid, process.getProcess())), + process.getProcess(), "", ""); + fail(); + } catch (StorageQueryException e) { + assertEquals(e.getMessage(), "java.sql.SQLException: com.zaxxer.hikari.pool" + + ".HikariPool$PoolInitializationException: Failed to initialize pool: FATAL: database " + + "\"random\" " + + "does not exist"); + } + + assertEquals(2, + Multitenancy.getAllTenants(new TenantIdentifier(null, null, null), process.getProcess()).length); + + + process.kill(false); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + { + 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)); + + assertEquals(2, + Multitenancy.getAllTenants(new TenantIdentifier(null, null, null), process.getProcess()).length); + + TenantIdentifier tid = new TenantIdentifier("abc", null, null); + try { + EmailPassword.signIn(tid.withStorage(StorageLayer.getStorage(tid, process.getProcess())), + process.getProcess(), "", ""); + fail(); + } catch (StorageQueryException e) { + assertEquals(e.getMessage(), "java.sql.SQLException: com.zaxxer.hikari.pool" + + ".HikariPool$PoolInitializationException: Failed to initialize pool: FATAL: database " + + "\"random\" " + + "does not exist"); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + } + + @Test + public void testBadPortWithNewTenantShouldNotCauseItToWaitInput() 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)); + + JsonObject tenantConfigJson = new JsonObject(); + tenantConfigJson.add("postgresql_port", new JsonPrimitive("8989")); + + TenantConfig tenantConfig = new TenantConfig(new TenantIdentifier("abc", null, null), + new EmailPasswordConfig(false), + new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), + new PasswordlessConfig(false), + tenantConfigJson); + + try { + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantIdentifier(null, null, null), + tenantConfig); + fail(); + } catch (StorageQueryException e) { + assertEquals(e.getMessage(), + "java.sql.SQLException: com.zaxxer.hikari.pool.HikariPool$PoolInitializationException: Failed to " + + "initialize pool: Connection to localhost:8989 refused. Check that the hostname and port " + + "are correct and that the postmaster is accepting TCP/IP connections."); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } } From af8c931b579789c33f916acfc08fb9889557bc8b Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Thu, 6 Apr 2023 19:18:58 +0530 Subject: [PATCH 056/106] fix: jwt changes (#82) --- .../supertokens/storage/postgresql/Start.java | 28 ++++++++-------- .../postgresql/queries/JWTSigningQueries.java | 32 ++++++++++++------- .../postgresql/test/ExceptionParsingTest.java | 6 +++- 3 files changed, 40 insertions(+), 26 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index ab085dbf..8fa95124 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -1328,10 +1328,9 @@ public boolean doesUserIdExist(TenantIdentifier tenantIdentifierIdentifier, Stri public List getJWTSigningKeys_Transaction(AppIdentifier appIdentifier, TransactionConnection con) throws StorageQueryException { - // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { - return JWTSigningQueries.getJWTSigningKeys_Transaction(this, sqlCon); + return JWTSigningQueries.getJWTSigningKeys_Transaction(this, sqlCon, appIdentifier); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1340,22 +1339,23 @@ public List getJWTSigningKeys_Transaction(AppIdentifier @Override public void setJWTSigningKey_Transaction(AppIdentifier appIdentifier, TransactionConnection con, JWTSigningKeyInfo info) - throws StorageQueryException, DuplicateKeyIdException { - // TODO... + throws StorageQueryException, DuplicateKeyIdException, TenantOrAppNotFoundException { Connection sqlCon = (Connection) con.getConnection(); try { - JWTSigningQueries.setJWTSigningKeyInfo_Transaction(this, sqlCon, info); + JWTSigningQueries.setJWTSigningKeyInfo_Transaction(this, sqlCon, appIdentifier, info); } catch (SQLException e) { - if (e instanceof PSQLException && isPrimaryKeyError(((PSQLException) e).getServerErrorMessage(), - Config.getConfig(this).getJWTSigningKeysTable())) { - throw new DuplicateKeyIdException(); - } - // We keep the old exception detection logic to ensure backwards compatibility. - // We could get here if the new logic hits a false negative, - // e.g., in case someone renamed constraints/tables - if (e.getMessage().contains("ERROR: duplicate key") && e.getMessage().contains("Key (key_id)")) { - throw new DuplicateKeyIdException(); + if (e instanceof PSQLException) { + PostgreSQLConfig config = Config.getConfig(this); + ServerErrorMessage serverMessage = ((PSQLException) e).getServerErrorMessage(); + + if (isPrimaryKeyError(serverMessage, config.getJWTSigningKeysTable())) { + throw new DuplicateKeyIdException(); + } + + if (isForeignKeyConstraintError(serverMessage, config.getJWTSigningKeysTable(), "app_id")) { + throw new TenantOrAppNotFoundException(appIdentifier); + } } throw new StorageQueryException(e); diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/JWTSigningQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/JWTSigningQueries.java index a3da0784..1b5d833c 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/JWTSigningQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/JWTSigningQueries.java @@ -21,6 +21,7 @@ import io.supertokens.pluginInterface.jwt.JWTAsymmetricSigningKeyInfo; import io.supertokens.pluginInterface.jwt.JWTSigningKeyInfo; import io.supertokens.pluginInterface.jwt.JWTSymmetricSigningKeyInfo; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.config.Config; import io.supertokens.storage.postgresql.utils.Utils; @@ -49,20 +50,27 @@ static String getQueryToCreateJWTSigningTable(Start start) { String jwtSigningKeysTable = Config.getConfig(start).getJWTSigningKeysTable(); // @formatter:off return "CREATE TABLE IF NOT EXISTS " + jwtSigningKeysTable + " (" - + "key_id VARCHAR(255) NOT NULL," + + "app_id VARCHAR(64) DEFAULT 'public'," + + "key_id VARCHAR(255) NOT NULL," + "key_string TEXT NOT NULL," + "algorithm VARCHAR(10) NOT NULL," + "created_at BIGINT," - + "CONSTRAINT " + Utils.getConstraintName(schema, jwtSigningKeysTable, null, "pkey") + " PRIMARY KEY(key_id));"; + + "CONSTRAINT " + Utils.getConstraintName(schema, jwtSigningKeysTable, null, "pkey") + + " PRIMARY KEY(app_id, key_id)," + + "CONSTRAINT " + Utils.getConstraintName(schema, jwtSigningKeysTable, "app_id", "fkey") + + " FOREIGN KEY(app_id)" + + " REFERENCES " + Config.getConfig(start).getAppsTable() + " (app_id) ON DELETE CASCADE" + + ");"; // @formatter:on } - public static List getJWTSigningKeys_Transaction(Start start, Connection con) + public static List getJWTSigningKeys_Transaction(Start start, Connection con, + AppIdentifier appIdentifier) throws SQLException, StorageQueryException { String QUERY = "SELECT * FROM " + getConfig(start).getJWTSigningKeysTable() - + " ORDER BY created_at DESC FOR UPDATE"; + + " WHERE app_id = ? ORDER BY created_at DESC FOR UPDATE"; - return execute(con, QUERY, NO_OP_SETTER, result -> { + return execute(con, QUERY, pst -> pst.setString(1, appIdentifier.getAppId()),result -> { List keys = new ArrayList<>(); while (result.next()) { @@ -98,17 +106,19 @@ public JWTSigningKeyInfo map(ResultSet result) throws Exception { } } - public static void setJWTSigningKeyInfo_Transaction(Start start, Connection con, JWTSigningKeyInfo info) + public static void setJWTSigningKeyInfo_Transaction(Start start, Connection con, AppIdentifier appIdentifier, + JWTSigningKeyInfo info) throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + getConfig(start).getJWTSigningKeysTable() - + "(key_id, key_string, created_at, algorithm) VALUES(?, ?, ?, ?)"; + + "(app_id, key_id, key_string, created_at, algorithm) VALUES(?, ?, ?, ?, ?)"; update(con, QUERY, pst -> { - pst.setString(1, info.keyId); - pst.setString(2, info.keyString); - pst.setLong(3, info.createdAtTime); - pst.setString(4, info.algorithm); + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, info.keyId); + pst.setString(3, info.keyString); + pst.setLong(4, info.createdAtTime); + pst.setString(5, info.algorithm); }); } } diff --git a/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java b/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java index 5db6103e..8afa705f 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java @@ -383,7 +383,7 @@ public void setJWTSigningKey_TransactionExceptions() throws InterruptedException TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - var storage = (JWTRecipeSQLStorage) StorageLayer.getJWTRecipeStorage(process.getProcess()); + var storage = (JWTRecipeSQLStorage) StorageLayer.getStorage(process.getProcess()); String keyId = "testkeyId"; String algorithm = "testalgo"; @@ -396,6 +396,8 @@ public void setJWTSigningKey_TransactionExceptions() throws InterruptedException storage.setJWTSigningKey_Transaction(new AppIdentifier(null, null), con, info); } catch (DuplicateKeyIdException e) { throw new StorageTransactionLogicException(e); + } catch (TenantOrAppNotFoundException e) { + throw new IllegalStateException(e); } try { @@ -403,6 +405,8 @@ public void setJWTSigningKey_TransactionExceptions() throws InterruptedException throw new StorageTransactionLogicException(new Exception("This should throw")); } catch (DuplicateKeyIdException e) { // expected + } catch (TenantOrAppNotFoundException e) { + throw new IllegalStateException(e); } return true; From 6ac571af1f112778b2efa703948338d43072fe8a Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Mon, 10 Apr 2023 11:23:43 +0530 Subject: [PATCH 057/106] fix: Multitenant General Queries (#84) * fix: updated general queries * fix: fixed queries --- .../supertokens/storage/postgresql/Start.java | 12 +- .../postgresql/queries/GeneralQueries.java | 155 ++++++++++++++---- 2 files changed, 130 insertions(+), 37 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 8fa95124..009c5ee5 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -1221,9 +1221,8 @@ public io.supertokens.pluginInterface.thirdparty.UserInfo[] getThirdPartyUsersBy @Override public long getUsersCount(TenantIdentifier tenantIdentifier, RECIPE_ID[] includeRecipeIds) throws StorageQueryException { - // TODO.. try { - return GeneralQueries.getUsersCount(this, includeRecipeIds); + return GeneralQueries.getUsersCount(this, tenantIdentifier, includeRecipeIds); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1232,9 +1231,8 @@ public long getUsersCount(TenantIdentifier tenantIdentifier, RECIPE_ID[] include @Override public long getUsersCount(AppIdentifier appIdentifier, RECIPE_ID[] includeRecipeIds) throws StorageQueryException { - // TODO.. try { - return GeneralQueries.getUsersCount(this, includeRecipeIds); + return GeneralQueries.getUsersCount(this, appIdentifier, includeRecipeIds); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1256,9 +1254,8 @@ public AuthRecipeUserInfo[] getUsers(TenantIdentifier tenantIdentifier, @NotNull @Override public boolean doesUserIdExist(AppIdentifier appIdentifier, String userId) throws StorageQueryException { - // TODO.. try { - return GeneralQueries.doesUserIdExist(this, userId); + return GeneralQueries.doesUserIdExist(this, appIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1316,9 +1313,8 @@ public int countUsersEnabledTotpAndActiveSince(AppIdentifier appIdentifier, long @Override public boolean doesUserIdExist(TenantIdentifier tenantIdentifierIdentifier, String userId) throws StorageQueryException { - // TODO:... try { - return GeneralQueries.doesUserIdExist(this, userId); + return GeneralQueries.doesUserIdExist(this, tenantIdentifierIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java index 92ac5566..4966160b 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java @@ -22,6 +22,7 @@ import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.dashboard.DashboardSearchTags; import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.storage.postgresql.ConnectionPool; import io.supertokens.storage.postgresql.Start; @@ -508,14 +509,14 @@ public static void deleteKeyValue_Transaction(Start start, Connection con, Tenan }); } - public static long getUsersCount(Start start, RECIPE_ID[] includeRecipeIds) + public static long getUsersCount(Start start, AppIdentifier appIdentifier, RECIPE_ID[] includeRecipeIds) throws SQLException, StorageQueryException { StringBuilder QUERY = new StringBuilder("SELECT COUNT(*) as total FROM " + getConfig(start).getUsersTable()); + QUERY.append(" WHERE app_id = ?"); if (includeRecipeIds != null && includeRecipeIds.length > 0) { - QUERY.append(" WHERE recipe_id IN ("); + QUERY.append(" AND recipe_id IN ("); for (int i = 0; i < includeRecipeIds.length; i++) { - String recipeId = includeRecipeIds[i].toString(); - QUERY.append("'").append(recipeId).append("'"); + QUERY.append("?"); if (i != includeRecipeIds.length - 1) { // not the last element QUERY.append(","); @@ -524,7 +525,15 @@ public static long getUsersCount(Start start, RECIPE_ID[] includeRecipeIds) QUERY.append(")"); } - return execute(start, QUERY.toString(), NO_OP_SETTER, result -> { + return execute(start, QUERY.toString(), pst -> { + pst.setString(1, appIdentifier.getAppId()); + if (includeRecipeIds != null) { + for (int i = 0; i < includeRecipeIds.length; i++) { + // i+2 cause this starts with 1 and not 0, and 1 is appId + pst.setString(i + 2, includeRecipeIds[i].toString()); + } + } + }, result -> { if (result.next()) { return result.getLong("total"); } @@ -532,11 +541,58 @@ public static long getUsersCount(Start start, RECIPE_ID[] includeRecipeIds) }); } - public static boolean doesUserIdExist(Start start, String userId) throws SQLException, StorageQueryException { + public static long getUsersCount(Start start, TenantIdentifier tenantIdentifier, RECIPE_ID[] includeRecipeIds) + throws SQLException, StorageQueryException { + StringBuilder QUERY = new StringBuilder("SELECT COUNT(*) as total FROM " + getConfig(start).getUsersTable()); + QUERY.append(" WHERE app_id = ? AND tenant_id = ?"); + if (includeRecipeIds != null && includeRecipeIds.length > 0) { + QUERY.append(" AND recipe_id IN ("); + for (int i = 0; i < includeRecipeIds.length; i++) { + QUERY.append("?"); + if (i != includeRecipeIds.length - 1) { + // not the last element + QUERY.append(","); + } + } + QUERY.append(")"); + } - String QUERY = "SELECT 1 FROM " + getConfig(start).getUsersTable() + " WHERE user_id = ?"; - return execute(start, QUERY, pst -> pst.setString(1, userId), ResultSet::next); + return execute(start, QUERY.toString(), pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + if (includeRecipeIds != null) { + for (int i = 0; i < includeRecipeIds.length; i++) { + // i+3 cause this starts with 1 and not 0, and 1 is appId, 2 is tenantId + pst.setString(i + 3, includeRecipeIds[i].toString()); + } + } + }, result -> { + if (result.next()) { + return result.getLong("total"); + } + return 0L; + }); + } + + public static boolean doesUserIdExist(Start start, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { + String QUERY = "SELECT 1 FROM " + getConfig(start).getUsersTable() + + " WHERE app_id = ? AND user_id = ?"; + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }, ResultSet::next); + } + + public static boolean doesUserIdExist(Start start, TenantIdentifier tenantIdentifier, String userId) throws SQLException, StorageQueryException { + + String QUERY = "SELECT 1 FROM " + getConfig(start).getUsersTable() + + " WHERE app_id = ? AND tenant_id = ? AND user_id = ?"; + return execute(start, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, userId); + }, ResultSet::next); } public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenantIdentifier, @NotNull Integer limit, @@ -559,11 +615,15 @@ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenant if (dashboardSearchTags.shouldEmailPasswordTableBeSearched()) { String QUERY = "SELECT allAuthUsersTable.*" + " FROM " + getConfig(start).getUsersTable() + " AS allAuthUsersTable" + - " JOIN " + getConfig(start).getEmailPasswordUsersTable() - + " AS emailpasswordTable ON allAuthUsersTable.user_id = emailpasswordTable.user_id"; + " JOIN " + getConfig(start).getEmailPasswordUserToTenantTable() + + " AS emailpasswordTable ON allAuthUsersTable.app_id = emailpasswordTable.app_id AND " + + "allAuthUsersTable.user_id = emailpasswordTable.user_id"; // attach email tags to queries - QUERY = QUERY + " WHERE emailpasswordTable.email LIKE ? OR emailpasswordTable.email LIKE ?"; + QUERY = QUERY + " WHERE (emailpasswordTable.app_id = ? AND emailpasswordTable.tenant_id = ?) AND" + + " (emailpasswordTable.email LIKE ? OR emailpasswordTable.email LIKE ?)"; + queryList.add(tenantIdentifier.getAppId()); + queryList.add(tenantIdentifier.getTenantId()); queryList.add(dashboardSearchTags.emails.get(0) + "%"); queryList.add("%@" + dashboardSearchTags.emails.get(0) + "%"); for (int i = 1; i < dashboardSearchTags.emails.size(); i++) { @@ -583,12 +643,19 @@ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenant String QUERY = "SELECT allAuthUsersTable.*" + " FROM " + getConfig(start).getUsersTable() + " AS allAuthUsersTable" + " JOIN " + getConfig(start).getThirdPartyUsersTable() - + " AS thirdPartyTable ON allAuthUsersTable.user_id = thirdPartyTable.user_id"; + + " AS thirdPartyTable ON allAuthUsersTable.app_id = thirdPartyTable.app_id AND" + + " allAuthUsersTable.user_id = thirdPartyTable.user_id" + + " JOIN " + getConfig(start).getThirdPartyUserToTenantTable() + + " AS thirdPartyToTenantTable ON thirdPartyTable.app_id = thirdPartyToTenantTable.app_id AND" + + " thirdPartyTable.user_id = thirdPartyToTenantTable.user_id"; // check if email tag is present if (dashboardSearchTags.emails != null) { - QUERY += " WHERE ( thirdPartyTable.email LIKE ? OR thirdPartyTable.email LIKE ?"; + QUERY += " WHERE (thirdPartyToTenantTable.app_id = ? AND thirdPartyToTenantTable.tenant_id = ?)" + + " AND ( thirdPartyTable.email LIKE ? OR thirdPartyTable.email LIKE ?"; + queryList.add(tenantIdentifier.getAppId()); + queryList.add(tenantIdentifier.getTenantId()); queryList.add(dashboardSearchTags.emails.get(0) + "%"); queryList.add("%@" + dashboardSearchTags.emails.get(0) + "%"); @@ -607,7 +674,9 @@ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenant if (dashboardSearchTags.emails != null) { QUERY += " AND "; } else { - QUERY += " WHERE "; + QUERY += " WHERE (thirdPartyToTenantTable.app_id = ? AND thirdPartyToTenantTable.tenant_id = ?) AND "; + queryList.add(tenantIdentifier.getAppId()); + queryList.add(tenantIdentifier.getTenantId()); } QUERY += " ( thirdPartyTable.third_party_id LIKE ?"; @@ -638,13 +707,17 @@ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenant if (dashboardSearchTags.shouldPasswordlessTableBeSearched()) { String QUERY = "SELECT allAuthUsersTable.*" + " FROM " + getConfig(start).getUsersTable() + " AS allAuthUsersTable" + - " JOIN " + getConfig(start).getPasswordlessUsersTable() - + " AS passwordlessTable ON allAuthUsersTable.user_id = passwordlessTable.user_id"; + " JOIN " + getConfig(start).getPasswordlessUserToTenantTable() + + " AS passwordlessTable ON allAuthUsersTable.app_id = passwordlessTable.app_id AND" + + " allAuthUsersTable.user_id = passwordlessTable.user_id"; // check if email tag is present if (dashboardSearchTags.emails != null) { - QUERY = QUERY + " WHERE ( passwordlessTable.email LIKE ? OR passwordlessTable.email LIKE ?"; + QUERY = QUERY + " WHERE (passwordlessTable.app_id = ? AND passwordlessTable.tenant_id = ?)" + + " AND ( passwordlessTable.email LIKE ? OR passwordlessTable.email LIKE ?"; + queryList.add(tenantIdentifier.getAppId()); + queryList.add(tenantIdentifier.getTenantId()); queryList.add(dashboardSearchTags.emails.get(0) + "%"); queryList.add("%@" + dashboardSearchTags.emails.get(0) + "%"); for (int i = 1; i < dashboardSearchTags.emails.size(); i++) { @@ -662,7 +735,9 @@ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenant if (dashboardSearchTags.emails != null) { QUERY += " AND "; } else { - QUERY += " WHERE "; + QUERY += " WHERE (passwordlessTable.app_id = ? AND passwordlessTable.tenant_id = ?) AND "; + queryList.add(tenantIdentifier.getAppId()); + queryList.add(tenantIdentifier.getTenantId()); } QUERY += " ( passwordlessTable.phone_number LIKE ?"; @@ -715,8 +790,8 @@ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenant if (includeRecipeIds != null && includeRecipeIds.length > 0) { RECIPE_ID_CONDITION.append("recipe_id IN ("); for (int i = 0; i < includeRecipeIds.length; i++) { - String recipeId = includeRecipeIds[i].toString(); - RECIPE_ID_CONDITION.append("'").append(recipeId).append("'"); + + RECIPE_ID_CONDITION.append("?"); if (i != includeRecipeIds.length - 1) { // not the last element RECIPE_ID_CONDITION.append(","); @@ -733,13 +808,23 @@ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenant String timeJoinedOrderSymbol = timeJoinedOrder.equals("ASC") ? ">" : "<"; String QUERY = "SELECT user_id, recipe_id FROM " + getConfig(start).getUsersTable() + " WHERE " + recipeIdCondition + " (time_joined " + timeJoinedOrderSymbol - + " ? OR (time_joined = ? AND user_id <= ?)) ORDER BY time_joined " + timeJoinedOrder + + " ? OR (time_joined = ? AND user_id <= ?)) AND app_id = ? AND tenant_id = ?" + + " ORDER BY time_joined " + timeJoinedOrder + ", user_id DESC LIMIT ?"; usersFromQuery = execute(start, QUERY, pst -> { - pst.setLong(1, timeJoined); - pst.setLong(2, timeJoined); - pst.setString(3, userId); - pst.setInt(4, limit); + if (includeRecipeIds != null) { + for (int i = 0; i < includeRecipeIds.length; i++) { + // i+1 cause this starts with 1 and not 0 + pst.setString(i + 1, includeRecipeIds[i].toString()); + } + } + int baseIndex = includeRecipeIds == null ? 0 : includeRecipeIds.length; + pst.setLong(baseIndex + 1, timeJoined); + pst.setLong(baseIndex + 2, timeJoined); + pst.setString(baseIndex + 3, userId); + pst.setString(baseIndex + 4, tenantIdentifier.getAppId()); + pst.setString(baseIndex + 5, tenantIdentifier.getTenantId()); + pst.setInt(baseIndex + 6, limit); }, result -> { List temp = new ArrayList<>(); while (result.next()) { @@ -750,12 +835,24 @@ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenant }); } else { String recipeIdCondition = RECIPE_ID_CONDITION.toString(); + String QUERY = "SELECT user_id, recipe_id FROM " + getConfig(start).getUsersTable() + " WHERE "; if (!recipeIdCondition.equals("")) { - recipeIdCondition = " WHERE " + recipeIdCondition; + QUERY += recipeIdCondition + " AND"; } - String QUERY = "SELECT user_id, recipe_id FROM " + getConfig(start).getUsersTable() + recipeIdCondition - + " ORDER BY time_joined " + timeJoinedOrder + ", user_id DESC LIMIT ?"; - usersFromQuery = execute(start, QUERY, pst -> pst.setInt(1, limit), result -> { + QUERY += " app_id = ? AND tenant_id = ? ORDER BY time_joined " + timeJoinedOrder + + ", user_id DESC LIMIT ?"; + usersFromQuery = execute(start, QUERY, pst -> { + if (includeRecipeIds != null) { + for (int i = 0; i < includeRecipeIds.length; i++) { + // i+1 cause this starts with 1 and not 0 + pst.setString(i + 1, includeRecipeIds[i].toString()); + } + } + int baseIndex = includeRecipeIds == null ? 0 : includeRecipeIds.length; + pst.setString(baseIndex + 1, tenantIdentifier.getAppId()); + pst.setString(baseIndex + 2, tenantIdentifier.getTenantId()); + pst.setInt(baseIndex + 3, limit); + }, result -> { List temp = new ArrayList<>(); while (result.next()) { temp.add(new UserInfoPaginationResultHolder(result.getString("user_id"), From 16e970d94e850a11387075cecf6a543ad815be26 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Mon, 10 Apr 2023 16:57:54 +0530 Subject: [PATCH 058/106] fix: Multitenant dashboard (#85) * fix: updated general queries * fix: fixed queries * fix: dashboard queries * fix: added fk contstraint * fix: fixed index --- .../supertokens/storage/postgresql/Start.java | 41 ++--- .../postgresql/queries/DashboardQueries.java | 142 +++++++++++------- 2 files changed, 107 insertions(+), 76 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 009c5ee5..043b8539 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -2294,9 +2294,8 @@ public void deleteConnectionUriDomain(String connectionUriDomain) throws @Override public boolean deleteDashboardUserWithUserId(AppIdentifier appIdentifier, String userId) throws StorageQueryException { - // TODO.. try { - return DashboardQueries.deleteDashboardUserWithUserId(this, userId); + return DashboardQueries.deleteDashboardUserWithUserId(this, appIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -2306,9 +2305,8 @@ public boolean deleteDashboardUserWithUserId(AppIdentifier appIdentifier, String public void createNewDashboardUserSession(AppIdentifier appIdentifier, String userId, String sessionId, long timeCreated, long expiry) throws StorageQueryException, UserIdNotFoundException { - // TODO.. try { - DashboardQueries.createDashboardSession(this, userId, sessionId, timeCreated, + DashboardQueries.createDashboardSession(this, appIdentifier, userId, sessionId, timeCreated, expiry); } catch (SQLException e) { if (e instanceof PSQLException) { @@ -2327,9 +2325,8 @@ public void createNewDashboardUserSession(AppIdentifier appIdentifier, String us @Override public DashboardSessionInfo[] getAllSessionsForUserId(AppIdentifier appIdentifier, String userId) throws StorageQueryException { - // TODO.. try { - return DashboardQueries.getAllSessionsForUserId(this, userId); + return DashboardQueries.getAllSessionsForUserId(this, appIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -2338,9 +2335,8 @@ public DashboardSessionInfo[] getAllSessionsForUserId(AppIdentifier appIdentifie @Override public DashboardSessionInfo getSessionInfoWithSessionId(AppIdentifier appIdentifier, String sessionId) throws StorageQueryException { - // TODO.. try { - return DashboardQueries.getSessionInfoWithSessionId(this, sessionId); + return DashboardQueries.getSessionInfoWithSessionId(this, appIdentifier, sessionId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -2349,9 +2345,8 @@ public DashboardSessionInfo getSessionInfoWithSessionId(AppIdentifier appIdentif @Override public boolean revokeSessionWithSessionId(AppIdentifier appIdentifier, String sessionId) throws StorageQueryException { - // TODO.. try { - return DashboardQueries.deleteDashboardUserSessionWithSessionId(this, + return DashboardQueries.deleteDashboardUserSessionWithSessionId(this, appIdentifier, sessionId); } catch (SQLException e) { throw new StorageQueryException(e); @@ -2365,11 +2360,10 @@ public void updateDashboardUsersEmailWithUserId_Transaction(AppIdentifier String newEmail) throws StorageQueryException, io.supertokens.pluginInterface.dashboard.exceptions.DuplicateEmailException, UserIdNotFoundException { - // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { if (!DashboardQueries.updateDashboardUsersEmailWithUserId_Transaction(this, - sqlCon, userId, newEmail)) { + sqlCon, appIdentifier, userId, newEmail)) { throw new UserIdNotFoundException(); } } catch (SQLException e) { @@ -2393,12 +2387,10 @@ public void updateDashboardUsersPasswordWithUserId_Transaction(AppIdentifier app TransactionConnection con, String userId, String newPassword) throws StorageQueryException, UserIdNotFoundException { - // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { if (!DashboardQueries.updateDashboardUsersPasswordWithUserId_Transaction(this, - sqlCon, userId, - newPassword)) { + sqlCon, appIdentifier, userId, newPassword)) { throw new UserIdNotFoundException(); } } catch (SQLException e) { @@ -2409,8 +2401,7 @@ public void updateDashboardUsersPasswordWithUserId_Transaction(AppIdentifier app @Override public DashboardUser[] getAllDashboardUsers(AppIdentifier appIdentifier) throws StorageQueryException { try { - // TODO.. - return DashboardQueries.getAllDashBoardUsers(this); + return DashboardQueries.getAllDashBoardUsers(this, appIdentifier); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -2420,8 +2411,7 @@ public DashboardUser[] getAllDashboardUsers(AppIdentifier appIdentifier) throws public DashboardUser getDashboardUserByUserId(AppIdentifier appIdentifier, String userId) throws StorageQueryException { try { - // TODO.. - return DashboardQueries.getDashboardUserByUserId(this, userId); + return DashboardQueries.getDashboardUserByUserId(this, appIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -2430,10 +2420,9 @@ public DashboardUser getDashboardUserByUserId(AppIdentifier appIdentifier, Strin @Override public void createNewDashboardUser(AppIdentifier appIdentifier, DashboardUser userInfo) throws StorageQueryException, io.supertokens.pluginInterface.dashboard.exceptions.DuplicateUserIdException, - io.supertokens.pluginInterface.dashboard.exceptions.DuplicateEmailException { - // TODO.. + io.supertokens.pluginInterface.dashboard.exceptions.DuplicateEmailException, TenantOrAppNotFoundException { try { - DashboardQueries.createDashboardUser(this, userInfo.userId, userInfo.email, + DashboardQueries.createDashboardUser(this, appIdentifier, userInfo.userId, userInfo.email, userInfo.passwordHash, userInfo.timeJoined); } catch (SQLException e) { @@ -2447,8 +2436,11 @@ public void createNewDashboardUser(AppIdentifier appIdentifier, DashboardUser us if (isUniqueConstraintError(serverErrorMessage, config.getDashboardUsersTable(), "email")) { throw new io.supertokens.pluginInterface.dashboard.exceptions.DuplicateEmailException(); - } + if (isForeignKeyConstraintError(serverErrorMessage, config.getDashboardUsersTable(), "app_id")) { + throw new TenantOrAppNotFoundException(appIdentifier); + } + } throw new StorageQueryException(e); } @@ -2459,8 +2451,7 @@ public void createNewDashboardUser(AppIdentifier appIdentifier, DashboardUser us public DashboardUser getDashboardUserByEmail(AppIdentifier appIdentifier, String email) throws StorageQueryException { try { - // TODO.. - return DashboardQueries.getDashboardUserByEmail(this, email); + return DashboardQueries.getDashboardUserByEmail(this, appIdentifier, email); } catch (SQLException e) { throw new StorageQueryException(e); } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/DashboardQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/DashboardQueries.java index 2d976a12..744e27a5 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/DashboardQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/DashboardQueries.java @@ -26,6 +26,7 @@ import io.supertokens.pluginInterface.dashboard.DashboardSessionInfo; import io.supertokens.pluginInterface.dashboard.DashboardUser; import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.storage.postgresql.QueryExecutorTemplate; import io.supertokens.storage.postgresql.ResultSetValueExtractor; import io.supertokens.storage.postgresql.Start; @@ -39,12 +40,19 @@ public static String getQueryToCreateDashboardUsersTable(Start start) { String dashboardUsersTable = Config.getConfig(start).getDashboardUsersTable(); // @formatter:off return "CREATE TABLE IF NOT EXISTS " + dashboardUsersTable + " (" + + "app_id VARCHAR(64) DEFAULT 'public'," + "user_id CHAR(36) NOT NULL," - + "email VARCHAR(256) NOT NULL CONSTRAINT " + - Utils.getConstraintName(schema, dashboardUsersTable, "email", "key") + " UNIQUE," - + "password_hash VARCHAR(256) NOT NULL," + "time_joined BIGINT NOT NULL," - + "CONSTRAINT " + Utils.getConstraintName(schema, dashboardUsersTable, null, "pkey") + - " PRIMARY KEY (user_id));"; + + "email VARCHAR(256) NOT NULL," + + "password_hash VARCHAR(256) NOT NULL," + + "time_joined BIGINT NOT NULL," + + "CONSTRAINT " + Utils.getConstraintName(schema, dashboardUsersTable, "email", "key") + + " UNIQUE (app_id, email)," + + "CONSTRAINT " + Utils.getConstraintName(schema, dashboardUsersTable, null, "pkey") + + " PRIMARY KEY (app_id, user_id)," + + "CONSTRAINT " + Utils.getConstraintName(schema, dashboardUsersTable, "app_id", "fkey") + + " FOREIGN KEY(app_id)" + + " REFERENCES " + Config.getConfig(start).getAppsTable() + " (app_id) ON DELETE CASCADE" + + ");"; // @formatter:on } @@ -53,14 +61,17 @@ public static String getQueryToCreateDashboardUserSessionsTable(Start start) { String tableName = Config.getConfig(start).getDashboardSessionsTable(); // @formatter:off return "CREATE TABLE IF NOT EXISTS " + tableName + " (" + + "app_id VARCHAR(64) DEFAULT 'public'," + "session_id CHAR(36) NOT NULL," + "user_id CHAR(36) NOT NULL," + "time_created BIGINT NOT NULL," + "expiry BIGINT NOT NULL," - + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, null, "pkey") + " PRIMARY KEY(session_id)," - + ("CONSTRAINT " + Utils.getConstraintName(schema, tableName, "user_id", "fkey") + " FOREIGN KEY (user_id)" - + " REFERENCES " + Config.getConfig(start).getDashboardUsersTable() + "(user_id)" - + " ON DELETE CASCADE ON UPDATE CASCADE);"); + + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, null, "pkey") + + " PRIMARY KEY(app_id, session_id)," + + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, "user_id", "fkey") + + " FOREIGN KEY (app_id, user_id)" + + " REFERENCES " + Config.getConfig(start).getDashboardUsersTable() + "(app_id, user_id)" + + " ON DELETE CASCADE ON UPDATE CASCADE);"; // @formatter:on } @@ -69,41 +80,51 @@ static String getQueryToCreateDashboardUserSessionsExpiryIndex(Start start) { + Config.getConfig(start).getDashboardSessionsTable() + "(expiry);"; } - public static void createDashboardUser(Start start, String userId, String email, String passwordHash, - long timeJoined) throws SQLException, StorageQueryException { + public static void createDashboardUser(Start start, AppIdentifier appIdentifier, String userId, String email, + String passwordHash, long timeJoined) + throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + Config.getConfig(start).getDashboardUsersTable() - + "(user_id, email, password_hash, time_joined)" + " VALUES(?, ?, ?, ?)"; + + "(app_id, user_id, email, password_hash, time_joined)" + " VALUES(?, ?, ?, ?, ?)"; QueryExecutorTemplate.update(start, QUERY, pst -> { - pst.setString(1, userId); - pst.setString(2, email); - pst.setString(3, passwordHash); - pst.setLong(4, timeJoined); + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + pst.setString(3, email); + pst.setString(4, passwordHash); + pst.setLong(5, timeJoined); }); } - public static boolean deleteDashboardUserWithUserId(Start start, String userId) + public static boolean deleteDashboardUserWithUserId(Start start, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { String QUERY = "DELETE FROM " + Config.getConfig(start).getDashboardUsersTable() - + " WHERE user_id = ?"; + + " WHERE app_id = ? AND user_id = ?"; // store the number of rows updated - int rowUpdatedCount = QueryExecutorTemplate.update(start, QUERY, pst -> pst.setString(1, userId)); + int rowUpdatedCount = QueryExecutorTemplate.update(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); return rowUpdatedCount > 0; }; - public static DashboardUser[] getAllDashBoardUsers(Start start) throws SQLException, StorageQueryException { + public static DashboardUser[] getAllDashBoardUsers(Start start, AppIdentifier appIdentifier) throws SQLException, StorageQueryException { String QUERY = "SELECT * FROM " - + Config.getConfig(start).getDashboardUsersTable() + " ORDER BY time_joined ASC"; - return QueryExecutorTemplate.execute(start, QUERY, null, new DashboardUserInfoResultExtractor()); + + Config.getConfig(start).getDashboardUsersTable() + " WHERE app_id = ? ORDER BY time_joined ASC"; + return QueryExecutorTemplate.execute(start, QUERY, + pst -> pst.setString(1, appIdentifier.getAppId()), + new DashboardUserInfoResultExtractor()); } - public static DashboardUser getDashboardUserByUserId(Start start, String userId) + public static DashboardUser getDashboardUserByUserId(Start start, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { String QUERY = "SELECT * FROM " - + Config.getConfig(start).getDashboardUsersTable() + " WHERE user_id = ?"; - return QueryExecutorTemplate.execute(start, QUERY, pst -> pst.setString(1, userId), result -> { + + Config.getConfig(start).getDashboardUsersTable() + " WHERE app_id = ? AND user_id = ?"; + return QueryExecutorTemplate.execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }, result -> { if (result.next()) { return DashboardInfoMapper.getInstance().mapOrThrow(result); } @@ -111,47 +132,57 @@ public static DashboardUser getDashboardUserByUserId(Start start, String userId) }); } - public static boolean updateDashboardUsersEmailWithUserId_Transaction(Start start, Connection con, String userId, - String newEmail) throws SQLException, StorageQueryException { + public static boolean updateDashboardUsersEmailWithUserId_Transaction(Start start, Connection con, + AppIdentifier appIdentifier, String userId, + String newEmail) + throws SQLException, StorageQueryException { String QUERY = "UPDATE " + Config.getConfig(start).getDashboardUsersTable() - + " SET email = ? WHERE user_id = ?"; + + " SET email = ? WHERE app_id = ? AND user_id = ?"; int rowsUpdated = QueryExecutorTemplate.update(con, QUERY, pst -> { pst.setString(1, newEmail); - pst.setString(2, userId); + pst.setString(2, appIdentifier.getAppId()); + pst.setString(3, userId); }); return rowsUpdated > 0; } - public static boolean updateDashboardUsersPasswordWithUserId_Transaction(Start start, Connection con, String userId, - String newPassword) throws SQLException, StorageQueryException { + public static boolean updateDashboardUsersPasswordWithUserId_Transaction(Start start, Connection con, + AppIdentifier appIdentifier, + String userId, String newPassword) + throws SQLException, StorageQueryException { String QUERY = "UPDATE " + Config.getConfig(start).getDashboardUsersTable() - + " SET password_hash = ? WHERE user_id = ?"; + + " SET password_hash = ? WHERE app_id = ? AND user_id = ?"; int rowsUpdated = QueryExecutorTemplate.update(con, QUERY, pst -> { pst.setString(1, newPassword); - pst.setString(2, userId); + pst.setString(2, appIdentifier.getAppId()); + pst.setString(3, userId); }); return rowsUpdated > 0; } - public static void createDashboardSession(Start start, String userId, String sessionId, long timeCreated, + public static void createDashboardSession(Start start, AppIdentifier appIdentifier, String userId, String sessionId, long timeCreated, long expiry) throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + Config.getConfig(start).getDashboardSessionsTable() - + "(user_id, session_id, time_created, expiry)" + " VALUES(?, ?, ?, ?)"; + + "(app_id, user_id, session_id, time_created, expiry)" + " VALUES(?, ?, ?, ?, ?)"; QueryExecutorTemplate.update(start, QUERY, pst -> { - pst.setString(1, userId); - pst.setString(2, sessionId); - pst.setLong(3, timeCreated); - pst.setLong(4, expiry); + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + pst.setString(3, sessionId); + pst.setLong(4, timeCreated); + pst.setLong(5, expiry); }); } - public static DashboardSessionInfo getSessionInfoWithSessionId(Start start, String sessionId) + public static DashboardSessionInfo getSessionInfoWithSessionId(Start start, AppIdentifier appIdentifier, String sessionId) throws SQLException, StorageQueryException { String QUERY = "SELECT * FROM " - + Config.getConfig(start).getDashboardSessionsTable() + " WHERE session_id = ?"; - return QueryExecutorTemplate.execute(start, QUERY, pst -> pst.setString(1, sessionId), result -> { + + Config.getConfig(start).getDashboardSessionsTable() + " WHERE app_id = ? AND session_id = ?"; + return QueryExecutorTemplate.execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, sessionId); + }, result -> { if (result.next()) { return DashboardSessionInfoMapper.getInstance().mapOrThrow(result); } @@ -159,11 +190,14 @@ public static DashboardSessionInfo getSessionInfoWithSessionId(Start start, Stri }); } - public static DashboardSessionInfo[] getAllSessionsForUserId(Start start, String userId) + public static DashboardSessionInfo[] getAllSessionsForUserId(Start start, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { String QUERY = "SELECT * FROM " - + Config.getConfig(start).getDashboardSessionsTable() + " WHERE user_id = ?"; - return QueryExecutorTemplate.execute(start, QUERY, pst -> pst.setString(1, userId), + + Config.getConfig(start).getDashboardSessionsTable() + " WHERE app_id = ? AND user_id = ?"; + return QueryExecutorTemplate.execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }, new DashboardSessionInfoResultExtractor()); } @@ -175,11 +209,14 @@ public static void deleteExpiredSessions(Start start) throws SQLException, Stora QueryExecutorTemplate.update(start, QUERY, pst -> pst.setLong(1, currentTimeMillis)); } - public static DashboardUser getDashboardUserByEmail(Start start, String email) + public static DashboardUser getDashboardUserByEmail(Start start, AppIdentifier appIdentifier, String email) throws SQLException, StorageQueryException { String QUERY = "SELECT * FROM " - + Config.getConfig(start).getDashboardUsersTable() + " WHERE email = ?"; - return QueryExecutorTemplate.execute(start, QUERY, pst -> pst.setString(1, email), result -> { + + Config.getConfig(start).getDashboardUsersTable() + " WHERE app_id = ? AND email = ?"; + return QueryExecutorTemplate.execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, email); + }, result -> { if (result.next()) { return DashboardInfoMapper.getInstance().mapOrThrow(result); } @@ -187,12 +224,15 @@ public static DashboardUser getDashboardUserByEmail(Start start, String email) }); } - public static boolean deleteDashboardUserSessionWithSessionId(Start start, String sessionId) + public static boolean deleteDashboardUserSessionWithSessionId(Start start, AppIdentifier appIdentifier, String sessionId) throws SQLException, StorageQueryException { String QUERY = "DELETE FROM " + Config.getConfig(start).getDashboardSessionsTable() - + " WHERE session_id = ?"; + + " WHERE app_id = ? AND session_id = ?"; // store the number of rows updated - int rowUpdatedCount = QueryExecutorTemplate.update(start, QUERY, pst -> pst.setString(1, sessionId)); + int rowUpdatedCount = QueryExecutorTemplate.update(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, sessionId); + }); return rowUpdatedCount > 0; } From 546de3700fe73497a28e74ce12b605b1d8c31a4a Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Tue, 11 Apr 2023 17:02:15 +0530 Subject: [PATCH 059/106] fix: Multitenant totp (#86) * fix: totp queries * fix: handling fk * fix: pr comment --- .../supertokens/storage/postgresql/Start.java | 44 ++--- .../postgresql/queries/TOTPQueries.java | 182 ++++++++++++------ .../storage/postgresql/test/DeadlockTest.java | 6 + .../postgresql/test/StorageLayerTest.java | 3 + 4 files changed, 150 insertions(+), 85 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 043b8539..e45156c4 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -686,7 +686,7 @@ public boolean isUserIdBeingUsedInNonAuthRecipe(AppIdentifier appIdentifier, Str } } else if (className.equals(TOTPStorage.class.getName())) { try { - TOTPDevice[] devices = TOTPQueries.getDevices(this, userId); + TOTPDevice[] devices = TOTPQueries.getDevices(this, appIdentifier, userId); return devices.length > 0; } catch (SQLException e) { throw new StorageQueryException(e); @@ -761,7 +761,7 @@ public void addInfoToNonAuthRecipesBasedOnUserId(String className, String userId } else if (className.equals(TOTPStorage.class.getName())) { try { TOTPDevice device = new TOTPDevice(userId, "testDevice", "secret", 0, 30, false); - TOTPQueries.createDevice(this, device); + TOTPQueries.createDevice(this, new AppIdentifier(null, null), device); } catch (StorageTransactionLogicException e) { throw new StorageQueryException(e.actualException); } @@ -2470,10 +2470,9 @@ public void revokeExpiredSessions() throws StorageQueryException { // TOTP recipe: @Override public void createDevice(AppIdentifier appIdentifier, TOTPDevice device) - throws StorageQueryException, DeviceAlreadyExistsException { + throws StorageQueryException, DeviceAlreadyExistsException, TenantOrAppNotFoundException { try { - // TODO.. - TOTPQueries.createDevice(this, device); + TOTPQueries.createDevice(this, appIdentifier, device); } catch (StorageTransactionLogicException e) { Exception actualException = e.actualException; @@ -2482,7 +2481,10 @@ public void createDevice(AppIdentifier appIdentifier, TOTPDevice device) if (isPrimaryKeyError(errMsg, Config.getConfig(this).getTotpUserDevicesTable())) { throw new DeviceAlreadyExistsException(); + } else if (isForeignKeyConstraintError(errMsg, Config.getConfig(this).getTotpUsersTable(), "app_id")) { + throw new TenantOrAppNotFoundException(appIdentifier); } + } throw new StorageQueryException(e.actualException); @@ -2493,8 +2495,7 @@ public void createDevice(AppIdentifier appIdentifier, TOTPDevice device) public void markDeviceAsVerified(AppIdentifier appIdentifier, String userId, String deviceName) throws StorageQueryException, UnknownDeviceException { try { - // TODO.. - int matchedCount = TOTPQueries.markDeviceAsVerified(this, userId, deviceName); + int matchedCount = TOTPQueries.markDeviceAsVerified(this, appIdentifier, userId, deviceName); if (matchedCount == 0) { // Note matchedCount != updatedCount throw new UnknownDeviceException(); @@ -2509,10 +2510,9 @@ public void markDeviceAsVerified(AppIdentifier appIdentifier, String userId, Str public int deleteDevice_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId, String deviceName) throws StorageQueryException { - // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { - return TOTPQueries.deleteDevice_Transaction(this, sqlCon, userId, deviceName); + return TOTPQueries.deleteDevice_Transaction(this, sqlCon, appIdentifier, userId, deviceName); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -2521,10 +2521,9 @@ public int deleteDevice_Transaction(TransactionConnection con, AppIdentifier app @Override public void removeUser_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId) throws StorageQueryException { - // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { - TOTPQueries.removeUser_Transaction(this, sqlCon, userId); + TOTPQueries.removeUser_Transaction(this, sqlCon, appIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -2534,9 +2533,8 @@ public void removeUser_Transaction(TransactionConnection con, AppIdentifier appI public void updateDeviceName(AppIdentifier appIdentifier, String userId, String oldDeviceName, String newDeviceName) throws StorageQueryException, DeviceAlreadyExistsException, UnknownDeviceException { - // TODO.. try { - int updatedCount = TOTPQueries.updateDeviceName(this, userId, oldDeviceName, newDeviceName); + int updatedCount = TOTPQueries.updateDeviceName(this, appIdentifier, userId, oldDeviceName, newDeviceName); if (updatedCount == 0) { throw new UnknownDeviceException(); } @@ -2553,9 +2551,8 @@ public void updateDeviceName(AppIdentifier appIdentifier, String userId, String @Override public TOTPDevice[] getDevices(AppIdentifier appIdentifier, String userId) throws StorageQueryException { - // TODO.. try { - return TOTPQueries.getDevices(this, userId); + return TOTPQueries.getDevices(this, appIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -2564,10 +2561,9 @@ public TOTPDevice[] getDevices(AppIdentifier appIdentifier, String userId) @Override public TOTPDevice[] getDevices_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId) throws StorageQueryException { - // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { - return TOTPQueries.getDevices_Transaction(this, sqlCon, userId); + return TOTPQueries.getDevices_Transaction(this, sqlCon, appIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -2576,11 +2572,11 @@ public TOTPDevice[] getDevices_Transaction(TransactionConnection con, AppIdentif @Override public void insertUsedCode_Transaction(TransactionConnection con, TenantIdentifier tenantIdentifier, TOTPUsedCode usedCodeObj) - throws StorageQueryException, TotpNotEnabledException, UsedCodeAlreadyExistsException { - // TODO.. + throws StorageQueryException, TotpNotEnabledException, UsedCodeAlreadyExistsException, + TenantOrAppNotFoundException { Connection sqlCon = (Connection) con.getConnection(); try { - TOTPQueries.insertUsedCode_Transaction(this, sqlCon, usedCodeObj); + TOTPQueries.insertUsedCode_Transaction(this, sqlCon, tenantIdentifier, usedCodeObj); } catch (SQLException e) { ServerErrorMessage err = ((PSQLException) e).getServerErrorMessage(); @@ -2589,6 +2585,8 @@ public void insertUsedCode_Transaction(TransactionConnection con, TenantIdentifi } else if (isForeignKeyConstraintError(err, Config.getConfig(this).getTotpUsedCodesTable(), "user_id")) { throw new TotpNotEnabledException(); + } else if (isForeignKeyConstraintError(err, Config.getConfig(this).getTotpUsedCodesTable(), "tenant_id")) { + throw new TenantOrAppNotFoundException(tenantIdentifier); } throw new StorageQueryException(e); @@ -2599,10 +2597,9 @@ public void insertUsedCode_Transaction(TransactionConnection con, TenantIdentifi public TOTPUsedCode[] getAllUsedCodesDescOrder_Transaction(TransactionConnection con, TenantIdentifier tenantIdentifier, String userId) throws StorageQueryException { - // TODO.. Connection sqlCon = (Connection) con.getConnection(); try { - return TOTPQueries.getAllUsedCodesDescOrder_Transaction(this, sqlCon, userId); + return TOTPQueries.getAllUsedCodesDescOrder_Transaction(this, sqlCon, tenantIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -2611,9 +2608,8 @@ public TOTPUsedCode[] getAllUsedCodesDescOrder_Transaction(TransactionConnection @Override public int removeExpiredCodes(TenantIdentifier tenantIdentifier, long expiredBefore) throws StorageQueryException { - // TODO.. try { - return TOTPQueries.removeExpiredCodes(this, expiredBefore); + return TOTPQueries.removeExpiredCodes(this, tenantIdentifier, expiredBefore); } catch (SQLException e) { throw new StorageQueryException(e); } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/TOTPQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/TOTPQueries.java index f4151ac4..2b7805ba 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/TOTPQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/TOTPQueries.java @@ -6,6 +6,8 @@ import java.util.ArrayList; import java.util.List; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.config.Config; import io.supertokens.pluginInterface.RowMapper; @@ -13,75 +15,113 @@ import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.totp.TOTPDevice; import io.supertokens.pluginInterface.totp.TOTPUsedCode; +import io.supertokens.storage.postgresql.utils.Utils; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.update; public class TOTPQueries { public static String getQueryToCreateUsersTable(Start start) { - return "CREATE TABLE IF NOT EXISTS " + Config.getConfig(start).getTotpUsersTable() + " (" + String schema = Config.getConfig(start).getTableSchema(); + String tableName = Config.getConfig(start).getTotpUsersTable(); + // @formatter:off + return "CREATE TABLE IF NOT EXISTS " + tableName + " (" + + "app_id VARCHAR(64) DEFAULT 'public'," + "user_id VARCHAR(128) NOT NULL," - + "PRIMARY KEY (user_id))"; + + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, null, "pkey") + + " PRIMARY KEY (app_id, user_id)," + + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, "app_id", "fkey") + + " FOREIGN KEY(app_id)" + + " REFERENCES " + Config.getConfig(start).getAppsTable() + " (app_id) ON DELETE CASCADE" + + ");"; + // @formatter:on } public static String getQueryToCreateUserDevicesTable(Start start) { - return "CREATE TABLE IF NOT EXISTS " + Config.getConfig(start).getTotpUserDevicesTable() + " (" - + "user_id VARCHAR(128) NOT NULL," + "device_name VARCHAR(256) NOT NULL," + String schema = Config.getConfig(start).getTableSchema(); + String tableName = Config.getConfig(start).getTotpUserDevicesTable(); + // @formatter:off + return "CREATE TABLE IF NOT EXISTS " + tableName + " (" + + "app_id VARCHAR(64) DEFAULT 'public'," + + "user_id VARCHAR(128) NOT NULL," + + "device_name VARCHAR(256) NOT NULL," + "secret_key VARCHAR(256) NOT NULL," - + "period INTEGER NOT NULL," + "skew INTEGER NOT NULL," + "verified BOOLEAN NOT NULL," - + "PRIMARY KEY (user_id, device_name)," - + "FOREIGN KEY (user_id) REFERENCES " - + Config.getConfig(start).getTotpUsersTable() + "(user_id) ON DELETE CASCADE);"; + + "period INTEGER NOT NULL," + + "skew INTEGER NOT NULL," + + "verified BOOLEAN NOT NULL," + + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, null, "pkey") + + " PRIMARY KEY (app_id, user_id, device_name)," + + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, "user_id", "fkey") + + " FOREIGN KEY (app_id, user_id)" + + " REFERENCES " + Config.getConfig(start).getTotpUsersTable() + "(app_id, user_id) ON DELETE CASCADE" + + ");"; + // @formatter:on } public static String getQueryToCreateUsedCodesTable(Start start) { - return "CREATE TABLE IF NOT EXISTS " + Config.getConfig(start).getTotpUsedCodesTable() + " (" + String schema = Config.getConfig(start).getTableSchema(); + String tableName = Config.getConfig(start).getTotpUsedCodesTable(); + // @formatter:off + return "CREATE TABLE IF NOT EXISTS " + tableName + " (" + + "app_id VARCHAR(64) DEFAULT 'public'," + + "tenant_id VARCHAR(64) DEFAULT 'public'," + "user_id VARCHAR(128) NOT NULL, " + "code VARCHAR(8) NOT NULL," + "is_valid BOOLEAN NOT NULL," + "expiry_time_ms BIGINT NOT NULL," + "created_time_ms BIGINT NOT NULL," - + "PRIMARY KEY (user_id, created_time_ms)," - + "FOREIGN KEY (user_id) REFERENCES " - + Config.getConfig(start).getTotpUsersTable() + "(user_id) ON DELETE CASCADE);"; + + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, null, "pkey") + + " PRIMARY KEY (app_id, tenant_id, user_id, created_time_ms)," + + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, "user_id", "fkey") + + " FOREIGN KEY (app_id, user_id)" + + " REFERENCES " + Config.getConfig(start).getTotpUsersTable() + "(app_id, user_id) ON DELETE CASCADE," + + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, "tenant_id", "fkey") + + " FOREIGN KEY (app_id, tenant_id)" + + " REFERENCES " + Config.getConfig(start).getTenantsTable() + " (app_id, tenant_id) ON DELETE CASCADE" + + ");"; + // @formatter:on } public static String getQueryToCreateUsedCodesExpiryTimeIndex(Start start) { return "CREATE INDEX IF NOT EXISTS totp_used_codes_expiry_time_ms_index ON " - + Config.getConfig(start).getTotpUsedCodesTable() + " (expiry_time_ms)"; + + Config.getConfig(start).getTotpUsedCodesTable() + " (app_id, tenant_id, expiry_time_ms)"; } - private static int insertUser_Transaction(Start start, Connection con, String userId) + private static int insertUser_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { // Create user if not exists: String QUERY = "INSERT INTO " + Config.getConfig(start).getTotpUsersTable() - + " (user_id) VALUES (?) ON CONFLICT DO NOTHING"; + + " (app_id, user_id) VALUES (?, ?) ON CONFLICT DO NOTHING"; - return update(con, QUERY, pst -> pst.setString(1, userId)); + return update(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); } - private static int insertDevice_Transaction(Start start, Connection con, TOTPDevice device) + private static int insertDevice_Transaction(Start start, Connection con, AppIdentifier appIdentifier, TOTPDevice device) throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + Config.getConfig(start).getTotpUserDevicesTable() - + " (user_id, device_name, secret_key, period, skew, verified) VALUES (?, ?, ?, ?, ?, ?)"; + + " (app_id, user_id, device_name, secret_key, period, skew, verified) VALUES (?, ?, ?, ?, ?, ?, ?)"; return update(con, QUERY, pst -> { - pst.setString(1, device.userId); - pst.setString(2, device.deviceName); - pst.setString(3, device.secretKey); - pst.setInt(4, device.period); - pst.setInt(5, device.skew); - pst.setBoolean(6, device.verified); + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, device.userId); + pst.setString(3, device.deviceName); + pst.setString(4, device.secretKey); + pst.setInt(5, device.period); + pst.setInt(6, device.skew); + pst.setBoolean(7, device.verified); }); } - public static void createDevice(Start start, TOTPDevice device) + public static void createDevice(Start start, AppIdentifier appIdentifier, TOTPDevice device) throws StorageQueryException, StorageTransactionLogicException { start.startTransaction(con -> { Connection sqlCon = (Connection) con.getConnection(); try { - insertUser_Transaction(start, sqlCon, device.userId); - insertDevice_Transaction(start, sqlCon, device); + insertUser_Transaction(start, sqlCon, appIdentifier, device.userId); + insertDevice_Transaction(start, sqlCon, appIdentifier, device); sqlCon.commit(); } catch (SQLException e) { throw new StorageTransactionLogicException(e); @@ -92,54 +132,63 @@ public static void createDevice(Start start, TOTPDevice device) return; } - public static int markDeviceAsVerified(Start start, String userId, String deviceName) + public static int markDeviceAsVerified(Start start, AppIdentifier appIdentifier, String userId, String deviceName) throws StorageQueryException, SQLException { String QUERY = "UPDATE " + Config.getConfig(start).getTotpUserDevicesTable() - + " SET verified = true WHERE user_id = ? AND device_name = ?"; + + " SET verified = true WHERE app_id = ? AND user_id = ? AND device_name = ?"; return update(start, QUERY, pst -> { - pst.setString(1, userId); - pst.setString(2, deviceName); + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + pst.setString(3, deviceName); }); } - public static int deleteDevice_Transaction(Start start, Connection con, String userId, String deviceName) + public static int deleteDevice_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String userId, String deviceName) throws SQLException, StorageQueryException { String QUERY = "DELETE FROM " + Config.getConfig(start).getTotpUserDevicesTable() - + " WHERE user_id = ? AND device_name = ?;"; + + " WHERE app_id = ? AND user_id = ? AND device_name = ?;"; return update(con, QUERY, pst -> { - pst.setString(1, userId); - pst.setString(2, deviceName); + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + pst.setString(3, deviceName); }); } - public static int removeUser_Transaction(Start start, Connection con, String userId) + public static int removeUser_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { String QUERY = "DELETE FROM " + Config.getConfig(start).getTotpUsersTable() - + " WHERE user_id = ?;"; - int removedUsersCount = update(con, QUERY, pst -> pst.setString(1, userId)); + + " WHERE app_id = ? AND user_id = ?;"; + int removedUsersCount = update(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); return removedUsersCount; } - public static int updateDeviceName(Start start, String userId, String oldDeviceName, String newDeviceName) + public static int updateDeviceName(Start start, AppIdentifier appIdentifier, String userId, String oldDeviceName, String newDeviceName) throws StorageQueryException, SQLException { String QUERY = "UPDATE " + Config.getConfig(start).getTotpUserDevicesTable() - + " SET device_name = ? WHERE user_id = ? AND device_name = ?;"; + + " SET device_name = ? WHERE app_id = ? AND user_id = ? AND device_name = ?;"; return update(start, QUERY, pst -> { pst.setString(1, newDeviceName); - pst.setString(2, userId); - pst.setString(3, oldDeviceName); + pst.setString(2, appIdentifier.getAppId()); + pst.setString(3, userId); + pst.setString(4, oldDeviceName); }); } - public static TOTPDevice[] getDevices(Start start, String userId) + public static TOTPDevice[] getDevices(Start start, AppIdentifier appIdentifier, String userId) throws StorageQueryException, SQLException { String QUERY = "SELECT * FROM " + Config.getConfig(start).getTotpUserDevicesTable() - + " WHERE user_id = ?;"; + + " WHERE app_id = ? AND user_id = ?;"; - return execute(start, QUERY, pst -> pst.setString(1, userId), result -> { + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }, result -> { List devices = new ArrayList<>(); while (result.next()) { devices.add(TOTPDeviceRowMapper.getInstance().map(result)); @@ -149,12 +198,15 @@ public static TOTPDevice[] getDevices(Start start, String userId) }); } - public static TOTPDevice[] getDevices_Transaction(Start start, Connection con, String userId) + public static TOTPDevice[] getDevices_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String userId) throws StorageQueryException, SQLException { String QUERY = "SELECT * FROM " + Config.getConfig(start).getTotpUserDevicesTable() - + " WHERE user_id = ? FOR UPDATE;"; + + " WHERE app_id = ? AND user_id = ? FOR UPDATE;"; - return execute(con, QUERY, pst -> pst.setString(1, userId), result -> { + return execute(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }, result -> { List devices = new ArrayList<>(); while (result.next()) { devices.add(TOTPDeviceRowMapper.getInstance().map(result)); @@ -165,17 +217,19 @@ public static TOTPDevice[] getDevices_Transaction(Start start, Connection con, S } - public static int insertUsedCode_Transaction(Start start, Connection con, TOTPUsedCode code) + public static int insertUsedCode_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, TOTPUsedCode code) throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + Config.getConfig(start).getTotpUsedCodesTable() - + " (user_id, code, is_valid, expiry_time_ms, created_time_ms) VALUES (?, ?, ?, ?, ?);"; + + " (app_id, tenant_id, user_id, code, is_valid, expiry_time_ms, created_time_ms) VALUES (?, ?, ?, ?, ?, ?, ?);"; return update(con, QUERY, pst -> { - pst.setString(1, code.userId); - pst.setString(2, code.code); - pst.setBoolean(3, code.isValid); - pst.setLong(4, code.expiryTime); - pst.setLong(5, code.createdTime); + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, code.userId); + pst.setString(4, code.code); + pst.setBoolean(5, code.isValid); + pst.setLong(6, code.expiryTime); + pst.setLong(7, code.createdTime); }); } @@ -184,14 +238,16 @@ public static int insertUsedCode_Transaction(Start start, Connection con, TOTPUs * order of creation time. */ public static TOTPUsedCode[] getAllUsedCodesDescOrder_Transaction(Start start, Connection con, - String userId) + TenantIdentifier tenantIdentifier, String userId) throws SQLException, StorageQueryException { // Take a lock based on the user id: String QUERY = "SELECT * FROM " + Config.getConfig(start).getTotpUsedCodesTable() - + " WHERE user_id = ? ORDER BY created_time_ms DESC FOR UPDATE;"; + + " WHERE app_id = ? AND tenant_id = ? AND user_id = ? ORDER BY created_time_ms DESC FOR UPDATE;"; return execute(con, QUERY, pst -> { - pst.setString(1, userId); + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, userId); }, result -> { List codes = new ArrayList<>(); while (result.next()) { @@ -202,12 +258,16 @@ public static TOTPUsedCode[] getAllUsedCodesDescOrder_Transaction(Start start, C }); } - public static int removeExpiredCodes(Start start, long expiredBefore) + public static int removeExpiredCodes(Start start, TenantIdentifier tenantIdentifier, long expiredBefore) throws StorageQueryException, SQLException { String QUERY = "DELETE FROM " + Config.getConfig(start).getTotpUsedCodesTable() - + " WHERE expiry_time_ms < ?;"; + + " WHERE app_id = ? AND tenant_id = ? AND expiry_time_ms < ?;"; - return update(start, QUERY, pst -> pst.setLong(1, expiredBefore)); + return update(start, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setLong(3, expiredBefore); + }); } private static class TOTPDeviceRowMapper implements RowMapper { diff --git a/src/test/java/io/supertokens/storage/postgresql/test/DeadlockTest.java b/src/test/java/io/supertokens/storage/postgresql/test/DeadlockTest.java index b094e87c..9ece4e63 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/DeadlockTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/DeadlockTest.java @@ -270,6 +270,8 @@ public void testConcurrentDeleteAndUpdate() throws Exception { } catch (TotpNotEnabledException | UsedCodeAlreadyExistsException e) { // This should not happen throw new StorageTransactionLogicException(e); + } catch (TenantOrAppNotFoundException e) { + throw new IllegalStateException(e); } return null; }); @@ -430,6 +432,8 @@ public void testConcurrentDeleteAndInsert() throws Exception { } catch (TotpNotEnabledException | UsedCodeAlreadyExistsException e) { // This should not happen throw new StorageTransactionLogicException(e); + } catch (TenantOrAppNotFoundException e) { + throw new IllegalStateException(e); } return null; }); @@ -531,6 +535,8 @@ public void testConcurrentDeleteAndInsert() throws Exception { } catch (TotpNotEnabledException | UsedCodeAlreadyExistsException e) { // This should not happen throw new StorageTransactionLogicException(e); + } catch (TenantOrAppNotFoundException e) { + throw new IllegalStateException(e); } sqlStorage.commitTransaction(con); t2State.set("commit"); diff --git a/src/test/java/io/supertokens/storage/postgresql/test/StorageLayerTest.java b/src/test/java/io/supertokens/storage/postgresql/test/StorageLayerTest.java index 0423fcc2..300d48e4 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/StorageLayerTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/StorageLayerTest.java @@ -6,6 +6,7 @@ import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.totp.TOTPDevice; import io.supertokens.pluginInterface.totp.TOTPUsedCode; import io.supertokens.pluginInterface.totp.exception.TotpNotEnabledException; @@ -46,6 +47,8 @@ public static void insertUsedCodeUtil(TOTPSQLStorage storage, TOTPUsedCode usedC return null; } catch (TotpNotEnabledException | UsedCodeAlreadyExistsException e) { throw new StorageTransactionLogicException(e); + } catch (TenantOrAppNotFoundException e) { + throw new IllegalStateException(e); } }); } catch (StorageTransactionLogicException e) { From 94855c91cac03d1a2113de63b8916bf7ee12d72b Mon Sep 17 00:00:00 2001 From: Rishabh Poddar Date: Thu, 13 Apr 2023 11:01:10 +0530 Subject: [PATCH 060/106] merges (#87) --- CHANGELOG.md | 19 ++++++++- build.gradle | 2 +- jar/postgresql-plugin-3.0.0.jar | Bin 0 -> 134651 bytes pluginInterfaceSupported.json | 2 +- .../supertokens/storage/postgresql/Start.java | 6 +-- .../postgresql/queries/GeneralQueries.java | 25 +++++++---- .../postgresql/queries/JWTSigningQueries.java | 2 +- .../postgresql/queries/SessionQueries.java | 40 ++++++++++++------ .../postgresql/test/InMemoryDBTest.java | 15 ++----- 9 files changed, 70 insertions(+), 41 deletions(-) create mode 100644 jar/postgresql-plugin-3.0.0.jar diff --git a/CHANGELOG.md b/CHANGELOG.md index ba3ea291..c1b659f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,23 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [3.0.0] - 2023-04-05 + +- Adds `use_static_key` `BOOLEAN` column into `session_info` +- Adds support for plugin inteface version 2.23 + +### Migration + +- If using `access_token_signing_key_dynamic` false in the core: + - `ALTER TABLE session_info ADD COLUMN use_static_key BOOLEAN NOT NULL DEFAULT(true);` + - ```sql + INSERT INTO jwt_signing_keys(key_id, key_string, algorithm, created_at) + select CONCAT('s-', created_at_time) as key_id, value as key_string, 'RS256' as algorithm, created_at_time as created_at + from session_access_token_signing_keys; + ``` +- If using `access_token_signing_key_dynamic` true in the core: + - `ALTER TABLE session_info ADD COLUMN use_static_key BOOLEAN NOT NULL DEFAULT(false);` + ## [2.4.0] - 2023-03-30 - Support for Dashboard Search @@ -171,4 +188,4 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixed -- The core now waits for the PostgrSQL db to start +- The core now waits for the PostgrSQL db to start \ No newline at end of file diff --git a/build.gradle b/build.gradle index af99adcb..5d0c815e 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'java-library' } -version = "2.4.0" +version = "3.0.0" repositories { mavenCentral() diff --git a/jar/postgresql-plugin-3.0.0.jar b/jar/postgresql-plugin-3.0.0.jar new file mode 100644 index 0000000000000000000000000000000000000000..34b051f73d9a4a5c735b446a02d6c3fa0ed864a4 GIT binary patch literal 134651 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%0Ajq$ZU}R%yDs9#aj^Yl&{~z4raU*q@-mNFQt1g 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 $an_09xmSx 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 literal 0 HcmV?d00001 diff --git a/pluginInterfaceSupported.json b/pluginInterfaceSupported.json index f7f92078..c86b532f 100644 --- a/pluginInterfaceSupported.json +++ b/pluginInterfaceSupported.json @@ -1,6 +1,6 @@ { "_comment": "contains a list of plugin interfaces branch names that this core supports", "versions": [ - "2.22" + "2.23" ] } \ No newline at end of file diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index e45156c4..1e6b1b3a 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -456,11 +456,11 @@ public void close() { public void createNewSession(TenantIdentifier tenantIdentifier, String sessionHandle, String userId, String refreshTokenHash2, JsonObject userDataInDatabase, long expiry, JsonObject userDataInJWT, - long createdAtTime) + long createdAtTime, boolean useStaticKey) throws StorageQueryException, TenantOrAppNotFoundException { try { SessionQueries.createNewSession(this, tenantIdentifier, sessionHandle, userId, refreshTokenHash2, - userDataInDatabase, expiry, userDataInJWT, createdAtTime); + userDataInDatabase, expiry, userDataInJWT, createdAtTime, useStaticKey); } catch (SQLException e) { if (e instanceof PSQLException) { PostgreSQLConfig config = Config.getConfig(this); @@ -706,7 +706,7 @@ public void addInfoToNonAuthRecipesBasedOnUserId(String className, String userId try { createNewSession(new TenantIdentifier(null, null, null), "sessionHandle", userId, "refreshTokenHash", new JsonObject(), - System.currentTimeMillis() + 1000000, new JsonObject(), System.currentTimeMillis()); + System.currentTimeMillis() + 1000000, new JsonObject(), System.currentTimeMillis(), false); } catch (Exception e) { throw new StorageQueryException(e); } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java index 4966160b..313c8e15 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java @@ -574,7 +574,8 @@ public static long getUsersCount(Start start, TenantIdentifier tenantIdentifier, }); } - public static boolean doesUserIdExist(Start start, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { + public static boolean doesUserIdExist(Start start, AppIdentifier appIdentifier, String userId) + throws SQLException, StorageQueryException { String QUERY = "SELECT 1 FROM " + getConfig(start).getUsersTable() + " WHERE app_id = ? AND user_id = ?"; @@ -584,7 +585,8 @@ public static boolean doesUserIdExist(Start start, AppIdentifier appIdentifier, }, ResultSet::next); } - public static boolean doesUserIdExist(Start start, TenantIdentifier tenantIdentifier, String userId) throws SQLException, StorageQueryException { + public static boolean doesUserIdExist(Start start, TenantIdentifier tenantIdentifier, String userId) + throws SQLException, StorageQueryException { String QUERY = "SELECT 1 FROM " + getConfig(start).getUsersTable() + " WHERE app_id = ? AND tenant_id = ? AND user_id = ?"; @@ -620,7 +622,8 @@ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenant + "allAuthUsersTable.user_id = emailpasswordTable.user_id"; // attach email tags to queries - QUERY = QUERY + " WHERE (emailpasswordTable.app_id = ? AND emailpasswordTable.tenant_id = ?) AND" + QUERY = QUERY + + " WHERE (emailpasswordTable.app_id = ? AND emailpasswordTable.tenant_id = ?) AND" + " (emailpasswordTable.email LIKE ? OR emailpasswordTable.email LIKE ?)"; queryList.add(tenantIdentifier.getAppId()); queryList.add(tenantIdentifier.getTenantId()); @@ -646,14 +649,18 @@ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenant + " AS thirdPartyTable ON allAuthUsersTable.app_id = thirdPartyTable.app_id AND" + " allAuthUsersTable.user_id = thirdPartyTable.user_id" + " JOIN " + getConfig(start).getThirdPartyUserToTenantTable() - + " AS thirdPartyToTenantTable ON thirdPartyTable.app_id = thirdPartyToTenantTable.app_id AND" + + + " AS thirdPartyToTenantTable ON thirdPartyTable.app_id = thirdPartyToTenantTable" + + ".app_id AND" + " thirdPartyTable.user_id = thirdPartyToTenantTable.user_id"; // check if email tag is present if (dashboardSearchTags.emails != null) { - QUERY += " WHERE (thirdPartyToTenantTable.app_id = ? AND thirdPartyToTenantTable.tenant_id = ?)" - + " AND ( thirdPartyTable.email LIKE ? OR thirdPartyTable.email LIKE ?"; + QUERY += + " WHERE (thirdPartyToTenantTable.app_id = ? AND thirdPartyToTenantTable.tenant_id" + + " = ?)" + + " AND ( thirdPartyTable.email LIKE ? OR thirdPartyTable.email LIKE ?"; queryList.add(tenantIdentifier.getAppId()); queryList.add(tenantIdentifier.getTenantId()); queryList.add(dashboardSearchTags.emails.get(0) + "%"); @@ -674,7 +681,8 @@ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenant if (dashboardSearchTags.emails != null) { QUERY += " AND "; } else { - QUERY += " WHERE (thirdPartyToTenantTable.app_id = ? AND thirdPartyToTenantTable.tenant_id = ?) AND "; + QUERY += " WHERE (thirdPartyToTenantTable.app_id = ? AND thirdPartyToTenantTable" + + ".tenant_id = ?) AND "; queryList.add(tenantIdentifier.getAppId()); queryList.add(tenantIdentifier.getTenantId()); } @@ -735,7 +743,8 @@ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenant if (dashboardSearchTags.emails != null) { QUERY += " AND "; } else { - QUERY += " WHERE (passwordlessTable.app_id = ? AND passwordlessTable.tenant_id = ?) AND "; + QUERY += " WHERE (passwordlessTable.app_id = ? AND passwordlessTable.tenant_id = ?) " + + "AND "; queryList.add(tenantIdentifier.getAppId()); queryList.add(tenantIdentifier.getTenantId()); } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/JWTSigningQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/JWTSigningQueries.java index 1b5d833c..a9cf7080 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/JWTSigningQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/JWTSigningQueries.java @@ -98,7 +98,7 @@ public JWTSigningKeyInfo map(ResultSet result) throws Exception { long createdAt = result.getLong("created_at"); String algorithm = result.getString("algorithm"); - if (keyString.contains("|")) { + if (keyString.contains("|") || keyString.contains(";")) { return new JWTAsymmetricSigningKeyInfo(keyId, createdAt, algorithm, keyString); } else { return new JWTSymmetricSigningKeyInfo(keyId, createdAt, algorithm, keyString); diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/SessionQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/SessionQueries.java index d39ad77b..bb42033c 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/SessionQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/SessionQueries.java @@ -56,6 +56,7 @@ public static String getQueryToCreateSessionInfoTable(Start start) { + "expires_at BIGINT NOT NULL," + "created_at_time BIGINT NOT NULL," + "jwt_user_payload TEXT," + + "use_static_key BOOLEAN NOT NULL," + "CONSTRAINT " + Utils.getConstraintName(schema, sessionInfoTable, null, "pkey") + " PRIMARY KEY(app_id, tenant_id, session_handle)," + "CONSTRAINT " + Utils.getConstraintName(schema, sessionInfoTable, "tenant_id", "fkey") @@ -83,12 +84,14 @@ static String getQueryToCreateAccessTokenSigningKeysTable(Start start) { // @formatter:on } - public static void createNewSession(Start start, TenantIdentifier tenantIdentifier, String sessionHandle, String userId, String refreshTokenHash2, - JsonObject userDataInDatabase, long expiry, JsonObject userDataInJWT, long createdAtTime) + public static void createNewSession(Start start, TenantIdentifier tenantIdentifier, String sessionHandle, + String userId, String refreshTokenHash2, + JsonObject userDataInDatabase, long expiry, JsonObject userDataInJWT, + long createdAtTime, boolean useStaticKey) throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + getConfig(start).getSessionInfoTable() + "(app_id, tenant_id, session_handle, user_id, refresh_token_hash_2, session_data, expires_at," - + " jwt_user_payload, created_at_time)" + " VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)"; + + " jwt_user_payload, created_at_time, use_static_key)" + " VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; update(start, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); @@ -100,6 +103,7 @@ public static void createNewSession(Start start, TenantIdentifier tenantIdentifi pst.setLong(7, expiry); pst.setString(8, userDataInJWT.toString()); pst.setLong(9, createdAtTime); + pst.setBoolean(10, useStaticKey); }); } @@ -107,7 +111,7 @@ public static SessionInfo getSessionInfo_Transaction(Start start, Connection con String sessionHandle) throws SQLException, StorageQueryException { String QUERY = "SELECT session_handle, user_id, refresh_token_hash_2, session_data, expires_at, " - + "created_at_time, jwt_user_payload FROM " + getConfig(start).getSessionInfoTable() + + "created_at_time, jwt_user_payload, use_static_key FROM " + getConfig(start).getSessionInfoTable() + " WHERE app_id = ? AND tenant_id = ? AND session_handle = ? FOR UPDATE"; return execute(con, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); @@ -123,7 +127,8 @@ public static SessionInfo getSessionInfo_Transaction(Start start, Connection con public static void updateSessionInfo_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, String sessionHandle, - String refreshTokenHash2, long expiry) throws SQLException, StorageQueryException { + String refreshTokenHash2, long expiry) + throws SQLException, StorageQueryException { String QUERY = "UPDATE " + getConfig(start).getSessionInfoTable() + " SET refresh_token_hash_2 = ?, expires_at = ?" + " WHERE app_id = ? AND tenant_id = ? AND session_handle = ?"; @@ -137,7 +142,8 @@ public static void updateSessionInfo_Transaction(Start start, Connection con, Te }); } - public static int getNumberOfSessions(Start start, TenantIdentifier tenantIdentifier) throws SQLException, StorageQueryException { + public static int getNumberOfSessions(Start start, TenantIdentifier tenantIdentifier) + throws SQLException, StorageQueryException { String QUERY = "SELECT count(*) as num FROM " + getConfig(start).getSessionInfoTable() + " WHERE app_id = ? AND tenant_id = ?"; @@ -152,7 +158,8 @@ public static int getNumberOfSessions(Start start, TenantIdentifier tenantIdenti }); } - public static int deleteSession(Start start, TenantIdentifier tenantIdentifier, String[] sessionHandles) throws SQLException, StorageQueryException { + public static int deleteSession(Start start, TenantIdentifier tenantIdentifier, String[] sessionHandles) + throws SQLException, StorageQueryException { if (sessionHandles.length == 0) { return 0; } @@ -176,7 +183,8 @@ public static int deleteSession(Start start, TenantIdentifier tenantIdentifier, }); } - public static void deleteSessionsOfUser(Start start, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { + public static void deleteSessionsOfUser(Start start, AppIdentifier appIdentifier, String userId) + throws SQLException, StorageQueryException { String QUERY = "DELETE FROM " + getConfig(start).getSessionInfoTable() + " WHERE app_id = ? AND user_id = ?"; @@ -186,7 +194,8 @@ public static void deleteSessionsOfUser(Start start, AppIdentifier appIdentifier }); } - public static String[] getAllNonExpiredSessionHandlesForUser(Start start, TenantIdentifier tenantIdentifier, String userId) + public static String[] getAllNonExpiredSessionHandlesForUser(Start start, TenantIdentifier tenantIdentifier, + String userId) throws SQLException, StorageQueryException { String QUERY = "SELECT session_handle FROM " + getConfig(start).getSessionInfoTable() + " WHERE app_id = ? AND tenant_id = ? AND user_id = ? AND expires_at >= ?"; @@ -209,7 +218,8 @@ public static String[] getAllNonExpiredSessionHandlesForUser(Start start, Tenant }); } - public static String[] getAllNonExpiredSessionHandlesForUser(Start start, AppIdentifier appIdentifier, String userId) + public static String[] getAllNonExpiredSessionHandlesForUser(Start start, AppIdentifier appIdentifier, + String userId) throws SQLException, StorageQueryException { String QUERY = "SELECT session_handle FROM " + getConfig(start).getSessionInfoTable() + " WHERE app_id = ? AND user_id = ? AND expires_at >= ?"; @@ -237,7 +247,8 @@ public static void deleteAllExpiredSessions(Start start) throws SQLException, St update(start, QUERY, pst -> pst.setLong(1, currentTimeMillis())); } - public static int updateSession(Start start, TenantIdentifier tenantIdentifier, String sessionHandle, @Nullable JsonObject sessionData, + public static int updateSession(Start start, TenantIdentifier tenantIdentifier, String sessionHandle, + @Nullable JsonObject sessionData, @Nullable JsonObject jwtPayload) throws SQLException, StorageQueryException { if (sessionData == null && jwtPayload == null) { @@ -271,9 +282,10 @@ public static int updateSession(Start start, TenantIdentifier tenantIdentifier, }); } - public static SessionInfo getSession(Start start, TenantIdentifier tenantIdentifier, String sessionHandle) throws SQLException, StorageQueryException { + public static SessionInfo getSession(Start start, TenantIdentifier tenantIdentifier, String sessionHandle) + throws SQLException, StorageQueryException { String QUERY = "SELECT session_handle, user_id, refresh_token_hash_2, session_data, expires_at, " - + "created_at_time, jwt_user_payload FROM " + getConfig(start).getSessionInfoTable() + + "created_at_time, jwt_user_payload, use_static_key FROM " + getConfig(start).getSessionInfoTable() + " WHERE app_id = ? AND tenant_id = ? AND session_handle = ?"; return execute(start, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); @@ -350,7 +362,7 @@ public SessionInfo map(ResultSet result) throws Exception { result.getString("refresh_token_hash_2"), jp.parse(result.getString("session_data")).getAsJsonObject(), result.getLong("expires_at"), jp.parse(result.getString("jwt_user_payload")).getAsJsonObject(), - result.getLong("created_at_time")); + result.getLong("created_at_time"), result.getBoolean("use_static_key")); } } diff --git a/src/test/java/io/supertokens/storage/postgresql/test/InMemoryDBTest.java b/src/test/java/io/supertokens/storage/postgresql/test/InMemoryDBTest.java index 14697ea4..affdb10b 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/InMemoryDBTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/InMemoryDBTest.java @@ -62,10 +62,7 @@ public void beforeEach() { } @Test - public void checkThatInMemDVWorksEvenIfWrongConfig() - throws InterruptedException, StorageQueryException, NoSuchAlgorithmException, InvalidKeyException, - SignatureException, InvalidAlgorithmParameterException, NoSuchPaddingException, BadPaddingException, - IOException, InvalidKeySpecException, IllegalBlockSizeException, StorageTransactionLogicException { + public void checkThatInMemDVWorksEvenIfWrongConfig() throws Exception { { Utils.commentConfigValue("postgresql_user"); Utils.commentConfigValue("postgresql_password"); @@ -108,10 +105,7 @@ public void checkThatInMemDVWorksEvenIfWrongConfig() } @Test - public void checkThatActualDBWorksIfCorrectConfigDev() throws InterruptedException, StorageQueryException, - NoSuchAlgorithmException, InvalidKeyException, SignatureException, InvalidAlgorithmParameterException, - NoSuchPaddingException, BadPaddingException, UnsupportedEncodingException, InvalidKeySpecException, - IllegalBlockSizeException, StorageTransactionLogicException { + public void checkThatActualDBWorksIfCorrectConfigDev() throws Exception { { String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); @@ -149,10 +143,7 @@ public void checkThatActualDBWorksIfCorrectConfigDev() throws InterruptedExcepti } @Test - public void checkThatActualDBWorksIfCorrectConfigProduction() throws InterruptedException, StorageQueryException, - NoSuchAlgorithmException, InvalidKeyException, SignatureException, InvalidAlgorithmParameterException, - NoSuchPaddingException, BadPaddingException, UnsupportedEncodingException, InvalidKeySpecException, - IllegalBlockSizeException, StorageTransactionLogicException { + public void checkThatActualDBWorksIfCorrectConfigProduction() throws Exception { { String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); From 9d1a3a27c9fd9b3372074aa978b84ea9a888545c Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 18 Apr 2023 17:56:27 +0530 Subject: [PATCH 061/106] adds new config --- .../supertokens/storage/postgresql/Start.java | 10 + .../test/SuperTokensSaaSSecretTest.java | 187 ++++++++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 src/test/java/io/supertokens/storage/postgresql/test/SuperTokensSaaSSecretTest.java diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 1e6b1b3a..dab12110 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -108,6 +108,11 @@ public class Start JWTRecipeSQLStorage, PasswordlessSQLStorage, UserMetadataSQLStorage, UserRolesSQLStorage, UserIdMappingStorage, MultitenancyStorage, DashboardSQLStorage, TOTPSQLStorage, ActiveUsersStorage { + // these configs are protected from being modified / viewed by the dev using the SuperTokens + // SaaS. If the core is not running in SuperTokens SaaS, this array has no effect. + private static String[] PROTECTED_DB_CONFIG = new String[]{"postgresql_connection_pool_size", + "postgresql_connection_uri", "postgresql_host", "postgresql_port", "postgresql_user", "postgresql_password", + "postgresql_database_name", "postgresql_table_schema"}; private static final Object appenderLock = new Object(); public static boolean silent = false; private ResourceDistributor resourceDistributor = new ResourceDistributor(); @@ -777,6 +782,11 @@ public void modifyConfigToAddANewUserPoolForTesting(JsonObject config, int poolN config.add("postgresql_database_name", new JsonPrimitive("st" + poolNumber)); } + @Override + public String[] getProtectedConfigsFromSuperTokensSaaSUsers() { + return PROTECTED_DB_CONFIG; + } + @Override public void signUp(TenantIdentifier tenantIdentifier, UserInfo userInfo) throws StorageQueryException, DuplicateUserIdException, DuplicateEmailException, diff --git a/src/test/java/io/supertokens/storage/postgresql/test/SuperTokensSaaSSecretTest.java b/src/test/java/io/supertokens/storage/postgresql/test/SuperTokensSaaSSecretTest.java new file mode 100644 index 00000000..7da71727 --- /dev/null +++ b/src/test/java/io/supertokens/storage/postgresql/test/SuperTokensSaaSSecretTest.java @@ -0,0 +1,187 @@ +/* + * 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.storage.postgresql.test; + +import com.google.gson.JsonObject; +import io.supertokens.ProcessState; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; +import io.supertokens.multitenancy.Multitenancy; +import io.supertokens.multitenancy.exception.BadPermissionException; +import io.supertokens.multitenancy.exception.CannotModifyBaseConfigException; +import io.supertokens.multitenancy.exception.DeletionInProgressException; +import io.supertokens.pluginInterface.exceptions.InvalidConfigException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.*; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.thirdparty.InvalidProviderConfigException; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import java.io.IOException; + +import static junit.framework.TestCase.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +public class SuperTokensSaaSSecretTest { + + private final String[] PROTECTED_DB_CONFIG = new String[]{"postgresql_connection_pool_size", + "postgresql_connection_uri", "postgresql_host", "postgresql_port", "postgresql_user", + "postgresql_password", + "postgresql_database_name", "postgresql_table_schema"}; + + private final Object[] PROTECTED_DB_CONFIG_VALUES = new Object[]{11, + "postgresql://root:root@localhost:5432/supertokens?currentSchema=myschema", "localhost", 5432, "root", + "root", "supertokens", "myschema"}; + + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + @Test + public void testThatTenantCannotSetDatabaseRelatedConfigIfSuperTokensSaaSSecretIsSet() + throws InterruptedException, IOException, InvalidConfigException, TenantOrAppNotFoundException, + InvalidProviderConfigException, DeletionInProgressException, StorageQueryException, + FeatureNotEnabledException, CannotModifyBaseConfigException { + String[] args = {"../"}; + + String saasSecret = "hg40239oirjgBHD9450=Beew123--hg40239oirjgBHD9450=Beew123--hg40239oirjgBHD9450=Beew123-"; + Utils.setValueInConfig("supertokens_saas_secret", saasSecret); + String apiKey = "hg40239oirjgBHD9450=Beew123--hg40239oiBeew123-"; + Utils.setValueInConfig("api_keys", apiKey); + 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)); + + for (int i = 0; i < PROTECTED_DB_CONFIG.length; i++) { + try { + JsonObject j = new JsonObject(); + j.addProperty(PROTECTED_DB_CONFIG[i], ""); + Multitenancy.addNewOrUpdateAppOrTenant(process.main, new TenantIdentifier(null, null, null), + new TenantConfig(new TenantIdentifier(null, null, "t1"), new EmailPasswordConfig(false), + new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), + new PasswordlessConfig(false), + j), true); + fail(); + } catch (BadPermissionException e) { + assertEquals(e.getMessage(), "Not allowed to modify DB related configs."); + } + } + + // TODO: we should call the API to add a new tenant with api key (not supertokens saas secret), and check + // that it fails too if we try and add the protected db configs. + + // TODO: we should call the API to add a new tenant with supertokens_saas_secret key and test that it passes. + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testThatTenantCanSetDatabaseRelatedConfigIfSuperTokensSaaSSecretIsNotSet() + throws InterruptedException, IOException, InvalidConfigException, TenantOrAppNotFoundException, + InvalidProviderConfigException, DeletionInProgressException, StorageQueryException, + FeatureNotEnabledException, CannotModifyBaseConfigException, BadPermissionException { + String[] args = {"../"}; + + String apiKey = "hg40239oirjgBHD9450=Beew123--hg40239oiBeew123-"; + Utils.setValueInConfig("api_keys", apiKey); + 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)); + + for (int i = 0; i < PROTECTED_DB_CONFIG.length; i++) { + JsonObject j = new JsonObject(); + if (PROTECTED_DB_CONFIG_VALUES[i] instanceof String) { + j.addProperty(PROTECTED_DB_CONFIG[i], (String) PROTECTED_DB_CONFIG_VALUES[i]); + } else if (PROTECTED_DB_CONFIG_VALUES[i] instanceof Integer) { + j.addProperty(PROTECTED_DB_CONFIG[i], (Integer) PROTECTED_DB_CONFIG_VALUES[i]); + } + Multitenancy.addNewOrUpdateAppOrTenant(process.main, new TenantIdentifier(null, null, null), + new TenantConfig(new TenantIdentifier(null, null, "t1"), new EmailPasswordConfig(false), + new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), + new PasswordlessConfig(false), + j), false); + } + + // TODO: we should call the API to add a new tenant with api key, and check + // that it passes and is allowed to read + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testThatTenantCannotGetDatabaseRelatedConfigIfSuperTokensSaaSSecretIsSet() + throws InterruptedException, IOException, InvalidConfigException, TenantOrAppNotFoundException, + InvalidProviderConfigException, DeletionInProgressException, StorageQueryException, + FeatureNotEnabledException, CannotModifyBaseConfigException, BadPermissionException { + String[] args = {"../"}; + + String saasSecret = "hg40239oirjgBHD9450=Beew123--hg40239oirjgBHD9450=Beew123--hg40239oirjgBHD9450=Beew123-"; + Utils.setValueInConfig("supertokens_saas_secret", saasSecret); + String apiKey = "hg40239oirjgBHD9450=Beew123--hg40239oiBeew123-"; + Utils.setValueInConfig("api_keys", apiKey); + 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)); + + for (int i = 0; i < PROTECTED_DB_CONFIG.length; i++) { + JsonObject j = new JsonObject(); + if (PROTECTED_DB_CONFIG_VALUES[i] instanceof String) { + j.addProperty(PROTECTED_DB_CONFIG[i], (String) PROTECTED_DB_CONFIG_VALUES[i]); + } else if (PROTECTED_DB_CONFIG_VALUES[i] instanceof Integer) { + j.addProperty(PROTECTED_DB_CONFIG[i], (Integer) PROTECTED_DB_CONFIG_VALUES[i]); + } + Multitenancy.addNewOrUpdateAppOrTenant(process.main, new TenantIdentifier(null, null, null), + new TenantConfig(new TenantIdentifier(null, null, "t" + i), new EmailPasswordConfig(false), + new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), + new PasswordlessConfig(false), + j)); + + // TODO: we should call the API to get tenant with just api key and check that + // that it does not return he protected props + + // TODO: We should call the API with the supertokens_saas_secret and check that it does return the db + // config. + + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } +} From 528ee86c31b9d21d33c71f70cc13b1f3447d230b Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Fri, 21 Apr 2023 12:46:25 +0530 Subject: [PATCH 062/106] fix: multitenancy changes (#88) * fix: multitenancy changes * fix: multitenant queries * fix: add userid to tenant * fix: saas test * fix: remove DeletionInProgressException * fix: pr comments * fix: recipe id in appid_to_userid table * fix: pr comment * fix: query fixes * fix: fixed validation * fix: added comments --- .../supertokens/storage/postgresql/Start.java | 149 ++++++++++++++---- .../queries/EmailPasswordQueries.java | 70 +++++++- .../postgresql/queries/GeneralQueries.java | 19 ++- .../queries/MultitenancyQueries.java | 49 +++++- .../queries/PasswordlessQueries.java | 73 ++++++++- .../postgresql/queries/ThirdPartyQueries.java | 75 ++++++++- .../test/SuperTokensSaaSSecretTest.java | 26 ++- .../test/multitenancy/StorageLayerTest.java | 10 +- 8 files changed, 412 insertions(+), 59 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index dab12110..b4e2bc8b 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -103,6 +103,8 @@ import java.util.List; import java.util.Set; +import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; + public class Start implements SessionSQLStorage, EmailPasswordSQLStorage, EmailVerificationSQLStorage, ThirdPartySQLStorage, JWTRecipeSQLStorage, PasswordlessSQLStorage, UserMetadataSQLStorage, UserRolesSQLStorage, UserIdMappingStorage, @@ -2208,10 +2210,10 @@ public void createTenant(TenantConfig tenantConfig) } @Override - public void addTenantIdInUserPool(TenantIdentifier tenantIdentifier) + public void addTenantIdInTargetStorage(TenantIdentifier tenantIdentifier) throws DuplicateTenantException, StorageQueryException { try { - MultitenancyQueries.addTenantIdInUserPool(this, tenantIdentifier); + MultitenancyQueries.addTenantIdInTargetStorage(this, tenantIdentifier); } catch (StorageTransactionLogicException e) { if (e.actualException instanceof PSQLException) { PostgreSQLConfig config = Config.getConfig(this); @@ -2224,12 +2226,6 @@ public void addTenantIdInUserPool(TenantIdentifier tenantIdentifier) } } - @Override - public void deleteTenantIdInUserPool(TenantIdentifier tenantIdentifier) throws - TenantOrAppNotFoundException { - // TODO: - } - @Override public void overwriteTenantConfig(TenantConfig tenantConfig) throws TenantOrAppNotFoundException, StorageQueryException, DuplicateThirdPartyIdException, @@ -2256,21 +2252,22 @@ public void overwriteTenantConfig(TenantConfig tenantConfig) } @Override - public void deleteTenant(TenantIdentifier tenantIdentifier) throws - TenantOrAppNotFoundException { - // TODO: + public void deleteTenantIdInTargetStorage(TenantIdentifier tenantIdentifier) throws StorageQueryException { + MultitenancyQueries.deleteTenantIdInTargetStorage(this, tenantIdentifier); } @Override - public void deleteApp(TenantIdentifier tenantIdentifier) throws - TenantOrAppNotFoundException { - // TODO: + public boolean deleteTenantInfoInBaseStorage(TenantIdentifier tenantIdentifier) throws StorageQueryException { + return MultitenancyQueries.deleteTenantConfig(this, tenantIdentifier); } @Override - public void deleteConnectionUriDomainMapping(TenantIdentifier tenantIdentifier) throws - TenantOrAppNotFoundException { - // TODO: + public boolean deleteAppInfoInBaseStorage(AppIdentifier appIdentifier) throws StorageQueryException { + return deleteTenantInfoInBaseStorage(appIdentifier.getAsPublicTenantIdentifier()); + } + @Override + public boolean deleteConnectionUriDomainInfoInBaseStorage(String connectionUriDomain) throws StorageQueryException { + return deleteTenantInfoInBaseStorage(new TenantIdentifier(connectionUriDomain, null, null)); } @Override @@ -2279,26 +2276,114 @@ public TenantConfig[] getAllTenants() throws StorageQueryException { } @Override - public void addUserIdToTenant(TenantIdentifier tenantIdentifier, String userId) - throws TenantOrAppNotFoundException, UnknownUserIdException { - // TODO: - } + public boolean addUserIdToTenant(TenantIdentifier tenantIdentifier, String userId) + throws TenantOrAppNotFoundException, UnknownUserIdException, StorageQueryException, + DuplicateEmailException, DuplicateThirdPartyUserException, DuplicatePhoneNumberException { + try { + return this.startTransaction(con -> { + Connection sqlCon = (Connection) con.getConnection(); + try { + String recipeId = GeneralQueries.getRecipeIdForUser_Transaction(this, sqlCon, tenantIdentifier, + userId); - @Override - public void addRoleToTenant(TenantIdentifier tenantIdentifier, String role) - throws TenantOrAppNotFoundException, UnknownRoleException { - // TODO: - } + if (recipeId == null) { + throw new StorageTransactionLogicException(new UnknownUserIdException()); + } - @Override - public void deleteAppId(String appId) throws TenantOrAppNotFoundException { - // TODO: + boolean added; + if (recipeId.equals("emailpassword")) { + added = EmailPasswordQueries.addUserIdToTenant_Transaction(this, sqlCon, tenantIdentifier, userId); + } else if (recipeId.equals("thirdparty")) { + added = ThirdPartyQueries.addUserIdToTenant_Transaction(this, sqlCon, tenantIdentifier, userId); + } else if (recipeId.equals("passwordless")) { + added = PasswordlessQueries.addUserIdToTenant_Transaction(this, sqlCon, tenantIdentifier, userId); + } else { + throw new IllegalStateException("Should never come here!"); + } + + sqlCon.commit(); + return added; + } catch (SQLException throwables) { + throw new StorageTransactionLogicException(throwables); + } + }); + } catch (StorageTransactionLogicException e) { + if (e.actualException instanceof SQLException) { + PostgreSQLConfig config = Config.getConfig(this); + ServerErrorMessage serverErrorMessage = ((PSQLException) e.actualException).getServerErrorMessage(); + + if (isForeignKeyConstraintError(serverErrorMessage, config.getUsersTable(), "tenant_id")) { + throw new TenantOrAppNotFoundException(tenantIdentifier); + } + if (isUniqueConstraintError(serverErrorMessage, config.getEmailPasswordUserToTenantTable(), "email")) { + throw new DuplicateEmailException(); + } + if (isUniqueConstraintError(serverErrorMessage, config.getThirdPartyUserToTenantTable(), "third_party_user_id")) { + throw new DuplicateThirdPartyUserException(); + } + if (isUniqueConstraintError(serverErrorMessage, + Config.getConfig(this).getPasswordlessUserToTenantTable(), "phone_number")) { + throw new DuplicatePhoneNumberException(); + } + if (isUniqueConstraintError(serverErrorMessage, + Config.getConfig(this).getPasswordlessUserToTenantTable(), "email")) { + throw new DuplicateEmailException(); + } + + throw new StorageQueryException(e.actualException); + } else if (e.actualException instanceof UnknownUserIdException) { + throw (UnknownUserIdException) e.actualException; + } else if (e.actualException instanceof StorageQueryException) { + throw (StorageQueryException) e.actualException; + } + throw new StorageQueryException(e.actualException); + } } @Override - public void deleteConnectionUriDomain(String connectionUriDomain) throws - TenantOrAppNotFoundException { - // TODO: + public boolean removeUserIdFromTenant(TenantIdentifier tenantIdentifier, String userId) + throws StorageQueryException, UnknownUserIdException { + try { + return this.startTransaction(con -> { + Connection sqlCon = (Connection) con.getConnection(); + try { + String recipeId = GeneralQueries.getRecipeIdForUser_Transaction(this, sqlCon, tenantIdentifier, + userId); + + if (recipeId == null) { + throw new StorageTransactionLogicException(new UnknownUserIdException()); + } + + boolean removed; + if (recipeId.equals("emailpassword")) { + removed = EmailPasswordQueries.removeUserIdFromTenant_Transaction(this, sqlCon, tenantIdentifier, userId); + } else if (recipeId.equals("thirdparty")) { + removed = ThirdPartyQueries.removeUserIdFromTenant_Transaction(this, sqlCon, tenantIdentifier, userId); + } else if (recipeId.equals("passwordless")) { + removed = PasswordlessQueries.removeUserIdFromTenant_Transaction(this, sqlCon, tenantIdentifier, userId); + } else { + throw new IllegalStateException("Should never come here!"); + } + + sqlCon.commit(); + return removed; + } catch (SQLException throwables) { + throw new StorageTransactionLogicException(throwables); + } + }); + } catch (StorageTransactionLogicException e) { + if (e.actualException instanceof SQLException) { + PostgreSQLConfig config = Config.getConfig(this); + ServerErrorMessage serverErrorMessage = ((PSQLException) e.actualException).getServerErrorMessage(); + + throw new StorageQueryException(e.actualException); + } else if (e.actualException instanceof UnknownUserIdException) { + throw (UnknownUserIdException) e.actualException; + } else if (e.actualException instanceof StorageQueryException) { + throw (StorageQueryException) e.actualException; + } + throw new StorageQueryException(e.actualException); + } } @Override diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java index 87aa6d18..03b90c50 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java @@ -255,10 +255,11 @@ public static void signUp(Start start, TenantIdentifier tenantIdentifier, String try { { // app_id_to_user_id String QUERY = "INSERT INTO " + getConfig(start).getAppIdToUserIdTable() - + "(app_id, user_id)" + " VALUES(?, ?)"; + + "(app_id, user_id, recipe_id)" + " VALUES(?, ?, ?)"; update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, userId); + pst.setString(3, EMAIL_PASSWORD.toString()); }); } @@ -345,6 +346,22 @@ public static UserInfo getUserInfoUsingId(Start start, AppIdentifier appIdentifi }); } + public static UserInfo getUserInfoUsingId(Start start, Connection sqlCon, AppIdentifier appIdentifier, String id) throws SQLException, StorageQueryException { + // we don't need a FOR UPDATE here because this is already part of a transaction, and locked on app_id_to_user_id table + String QUERY = "SELECT user_id, email, password_hash, time_joined FROM " + + getConfig(start).getEmailPasswordUsersTable() + " WHERE app_id = ? AND user_id = ?"; + + return execute(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, id); + }, result -> { + if (result.next()) { + return UserInfoRowMapper.getInstance().mapOrThrow(result); + } + return null; + }); + } + public static List getUsersInfoUsingIdList(Start start, List ids) throws SQLException, StorageQueryException { if (ids.size() > 0) { @@ -398,6 +415,57 @@ public static UserInfo getUserInfoUsingEmail(Start start, TenantIdentifier tenan }); } + public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlCon, + TenantIdentifier tenantIdentifier, String userId) + throws SQLException, StorageQueryException { + UserInfo userInfo = EmailPasswordQueries.getUserInfoUsingId(start, sqlCon, + tenantIdentifier.toAppIdentifier(), userId); + + { // all_auth_recipe_users + String QUERY = "INSERT INTO " + getConfig(start).getUsersTable() + + "(app_id, tenant_id, user_id, recipe_id, time_joined)" + + " VALUES(?, ?, ?, ?, ?)" + " ON CONFLICT DO NOTHING"; + update(sqlCon, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, userId); + pst.setString(4, EMAIL_PASSWORD.toString()); + pst.setLong(5, userInfo.timeJoined); + }); + } + + { // emailpassword_user_to_tenant + String QUERY = "INSERT INTO " + getConfig(start).getEmailPasswordUserToTenantTable() + + "(app_id, tenant_id, user_id, email)" + + " VALUES(?, ?, ?, ?) " + " ON CONFLICT DO NOTHING"; + + int numRows = update(sqlCon, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, userId); + pst.setString(4, userInfo.email); + }); + + return numRows > 0; + } + } + + public static boolean removeUserIdFromTenant_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId) + throws SQLException, StorageQueryException { + { // all_auth_recipe_users + String QUERY = "DELETE FROM " + getConfig(start).getUsersTable() + + " WHERE app_id = ? AND tenant_id = ? and user_id = ? and recipe_id = ?"; + int numRows = update(sqlCon, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, userId); + pst.setString(4, EMAIL_PASSWORD.toString()); + }); + return numRows > 0; + } + // automatically deleted from emailpassword_user_to_tenant because of foreign key constraint + } + private static class PasswordResetRowMapper implements RowMapper { public static final PasswordResetRowMapper INSTANCE = new PasswordResetRowMapper(); diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java index 313c8e15..9392caf9 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java @@ -150,6 +150,7 @@ private static String getQueryToCreateAppIdToUserIdTable(Start start) { return "CREATE TABLE IF NOT EXISTS " + appToUserTable + " (" + "app_id VARCHAR(64) NOT NULL DEFAULT 'public'," + "user_id CHAR(36) NOT NULL," + + "recipe_id VARCHAR(128) NOT NULL," + "CONSTRAINT " + Utils.getConstraintName(schema, appToUserTable, null, "pkey") + " PRIMARY KEY (app_id, user_id), " + "CONSTRAINT " + Utils.getConstraintName(schema, appToUserTable, "app_id", "fkey") @@ -577,7 +578,7 @@ public static long getUsersCount(Start start, TenantIdentifier tenantIdentifier, public static boolean doesUserIdExist(Start start, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { - String QUERY = "SELECT 1 FROM " + getConfig(start).getUsersTable() + String QUERY = "SELECT 1 FROM " + getConfig(start).getAppIdToUserIdTable() + " WHERE app_id = ? AND user_id = ?"; return execute(start, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); @@ -926,6 +927,22 @@ private static List getUserInfoForRecipeIdFromUser } } + public static String getRecipeIdForUser_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId) + throws SQLException, StorageQueryException { + String QUERY = "SELECT recipe_id FROM " + getConfig(start).getAppIdToUserIdTable() + + " WHERE app_id = ? AND user_id = ? FOR UPDATE"; + + return execute(sqlCon, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, userId); + }, result -> { + if (result.next()) { + return result.getString("recipe_id"); + } + return null; + }); + } + private static class UserInfoPaginationResultHolder { String userId; String recipeId; diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/MultitenancyQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/MultitenancyQueries.java index c72cd645..3db38834 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/MultitenancyQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/MultitenancyQueries.java @@ -16,6 +16,7 @@ package io.supertokens.storage.postgresql.queries; +import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.*; @@ -32,6 +33,7 @@ import java.util.HashMap; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.update; +import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; import static io.supertokens.storage.postgresql.config.Config.getConfig; public class MultitenancyQueries { @@ -137,6 +139,24 @@ public static void createTenantConfig(Start start, TenantConfig tenantConfig) th }); } + public static boolean deleteTenantConfig(Start start, TenantIdentifier tenantIdentifier) throws StorageQueryException { + try { + String QUERY = "DELETE FROM " + getConfig(start).getTenantConfigsTable() + + " WHERE connection_uri_domain = ? AND app_id = ? AND tenant_id = ?"; + + int numRows = update(start, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getConnectionUriDomain()); + pst.setString(2, tenantIdentifier.getAppId()); + pst.setString(3, tenantIdentifier.getTenantId()); + }); + + return numRows > 0; + + } catch (SQLException throwables) { + throw new StorageQueryException(throwables); + } + } + public static void overwriteTenantConfig(Start start, TenantConfig tenantConfig) throws StorageQueryException, StorageTransactionLogicException { start.startTransaction(con -> { Connection sqlCon = (Connection) con.getConnection(); @@ -185,7 +205,7 @@ public static TenantConfig[] getAllTenants(Start start) throws StorageQueryExcep } } - public static void addTenantIdInUserPool(Start start, TenantIdentifier tenantIdentifier) throws + public static void addTenantIdInTargetStorage(Start start, TenantIdentifier tenantIdentifier) throws StorageTransactionLogicException, StorageQueryException { { start.startTransaction(con -> { @@ -220,4 +240,31 @@ public static void addTenantIdInUserPool(Start start, TenantIdentifier tenantIde }); } } + + public static void deleteTenantIdInTargetStorage(Start start, TenantIdentifier tenantIdentifier) + throws StorageQueryException { + try { + if (tenantIdentifier.getTenantId().equals(TenantIdentifier.DEFAULT_TENANT_ID)) { + // Delete the app + String QUERY = "DELETE FROM " + getConfig(start).getAppsTable() + + " WHERE app_id = ?"; + + update(start, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + }); + } else { + // Delete the tenant + String QUERY = "DELETE FROM " + getConfig(start).getTenantsTable() + + " WHERE app_id = ? AND tenant_id = ?"; + + update(start, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + }); + } + + } catch (SQLException throwables) { + throw new StorageQueryException(throwables); + } + } } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java index cb06cd80..8dddd1c5 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java @@ -369,10 +369,11 @@ public static void createUser(Start start, TenantIdentifier tenantIdentifier, Us try { { // app_id_to_user_id String QUERY = "INSERT INTO " + getConfig(start).getAppIdToUserIdTable() - + "(app_id, user_id)" + " VALUES(?, ?)"; + + "(app_id, user_id, recipe_id)" + " VALUES(?, ?, ?)"; update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, user.id); + pst.setString(3, PASSWORDLESS.toString()); }); } @@ -705,6 +706,23 @@ public static UserInfo getUserById(Start start, AppIdentifier appIdentifier, Str }); } + public static UserInfo getUserById(Start start, Connection sqlCon, AppIdentifier appIdentifier, String userId) throws StorageQueryException, SQLException { + // we don't need a FOR UPDATE here because this is already part of a transaction, and locked on app_id_to_user_id table + String QUERY = "SELECT user_id, email, phone_number, time_joined FROM " + + getConfig(start).getPasswordlessUsersTable() + + " WHERE app_id = ? AND user_id = ?"; + + return execute(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }, result -> { + if (result.next()) { + return UserInfoRowMapper.getInstance().mapOrThrow(result); + } + return null; + }); + } + public static UserInfo getUserByEmail(Start start, TenantIdentifier tenantIdentifier, @Nonnull String email) throws StorageQueryException, SQLException { String QUERY = "SELECT pl_users.user_id as user_id, pl_users.email as email, " @@ -747,6 +765,59 @@ public static UserInfo getUserByPhoneNumber(Start start, TenantIdentifier tenant }); } + public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId) + throws StorageQueryException, SQLException { + UserInfo userInfo = PasswordlessQueries.getUserById(start, sqlCon, + tenantIdentifier.toAppIdentifier(), userId); + + { // all_auth_recipe_users + String QUERY = "INSERT INTO " + getConfig(start).getUsersTable() + + "(app_id, tenant_id, user_id, recipe_id, time_joined)" + + " VALUES(?, ?, ?, ?, ?)" + " ON CONFLICT DO NOTHING"; + update(sqlCon, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, userInfo.id); + pst.setString(4, PASSWORDLESS.toString()); + pst.setLong(5, userInfo.timeJoined); + }); + } + + { // passwordless_user_to_tenant + String QUERY = "INSERT INTO " + getConfig(start).getPasswordlessUserToTenantTable() + + "(app_id, tenant_id, user_id, email, phone_number)" + + " VALUES(?, ?, ?, ?, ?)" + " ON CONFLICT DO NOTHING"; + + int numRows = update(sqlCon, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, userInfo.id); + pst.setString(4, userInfo.email); + pst.setString(5, userInfo.phoneNumber); + }); + + return numRows > 0; + } + } + + public static boolean removeUserIdFromTenant_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId) + throws SQLException, StorageQueryException { + { // all_auth_recipe_users + String QUERY = "DELETE FROM " + getConfig(start).getUsersTable() + + " WHERE app_id = ? AND tenant_id = ? and user_id = ? and recipe_id = ?"; + int numRows = update(sqlCon, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, userId); + pst.setString(4, PASSWORDLESS.toString()); + }); + + return numRows > 0; + } + + // automatically deleted from passwordless_user_to_tenant because of foreign key constraint + } + private static class PasswordlessDeviceRowMapper implements RowMapper { private static final PasswordlessDeviceRowMapper INSTANCE = new PasswordlessDeviceRowMapper(); diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/ThirdPartyQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/ThirdPartyQueries.java index 3339cde0..f5a3f9c4 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/ThirdPartyQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/ThirdPartyQueries.java @@ -22,7 +22,6 @@ import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.thirdparty.UserInfo; -import io.supertokens.storage.postgresql.ConnectionPool; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.config.Config; import io.supertokens.storage.postgresql.utils.Utils; @@ -100,10 +99,11 @@ public static void signUp(Start start, TenantIdentifier tenantIdentifier, UserIn try { { // app_id_to_user_id String QUERY = "INSERT INTO " + getConfig(start).getAppIdToUserIdTable() - + "(app_id, user_id)" + " VALUES(?, ?)"; + + "(app_id, user_id, recipe_id)" + " VALUES(?, ?, ?)"; update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, userInfo.id); + pst.setString(3, THIRD_PARTY.toString()); }); } @@ -287,6 +287,25 @@ public static UserInfo getUserInfoUsingId_Transaction(Start start, Connection co }); } + public static UserInfo getUserInfoUsingUserId(Start start, Connection con, + AppIdentifier appIdentifier, String userId) + throws SQLException, StorageQueryException { + + // we don't need a FOR UPDATE here because this is already part of a transaction, and locked on app_id_to_user_id table + String QUERY = "SELECT user_id, third_party_id, third_party_user_id, email, time_joined FROM " + + getConfig(start).getThirdPartyUsersTable() + + " WHERE app_id = ? AND user_id = ?"; + return execute(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }, result -> { + if (result.next()) { + return UserInfoRowMapper.getInstance().mapOrThrow(result); + } + return null; + }); + } + public static UserInfo[] getThirdPartyUsersByEmail(Start start, TenantIdentifier tenantIdentifier, @NotNull String email) throws SQLException, StorageQueryException { @@ -313,6 +332,58 @@ public static UserInfo[] getThirdPartyUsersByEmail(Start start, TenantIdentifier }); } + public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId) + throws SQLException, StorageQueryException { + UserInfo userInfo = ThirdPartyQueries.getUserInfoUsingUserId(start, sqlCon, + tenantIdentifier.toAppIdentifier(), userId); + + { // all_auth_recipe_users + String QUERY = "INSERT INTO " + getConfig(start).getUsersTable() + + "(app_id, tenant_id, user_id, recipe_id, time_joined)" + + " VALUES(?, ?, ?, ?, ?)" + " ON CONFLICT DO NOTHING"; + update(sqlCon, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, userInfo.id); + pst.setString(4, THIRD_PARTY.toString()); + pst.setLong(5, userInfo.timeJoined); + }); + } + + { // thirdparty_user_to_tenant + String QUERY = "INSERT INTO " + getConfig(start).getThirdPartyUserToTenantTable() + + "(app_id, tenant_id, user_id, third_party_id, third_party_user_id)" + + " VALUES(?, ?, ?, ?, ?)" + " ON CONFLICT DO NOTHING"; + int numRows = update(sqlCon, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, userInfo.id); + pst.setString(4, userInfo.thirdParty.id); + pst.setString(5, userInfo.thirdParty.userId); + }); + + return numRows > 0; + } + } + + public static boolean removeUserIdFromTenant_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId) + throws SQLException, StorageQueryException { + { // all_auth_recipe_users + String QUERY = "DELETE FROM " + getConfig(start).getUsersTable() + + " WHERE app_id = ? AND tenant_id = ? and user_id = ? and recipe_id = ?"; + int numRows = update(sqlCon, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, userId); + pst.setString(4, THIRD_PARTY.toString()); + }); + + return numRows > 0; + } + + // automatically deleted from thirdparty_user_to_tenant because of foreign key constraint + } + private static class UserInfoRowMapper implements RowMapper { private static final UserInfoRowMapper INSTANCE = new UserInfoRowMapper(); diff --git a/src/test/java/io/supertokens/storage/postgresql/test/SuperTokensSaaSSecretTest.java b/src/test/java/io/supertokens/storage/postgresql/test/SuperTokensSaaSSecretTest.java index 7da71727..b098e547 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/SuperTokensSaaSSecretTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/SuperTokensSaaSSecretTest.java @@ -24,7 +24,6 @@ import io.supertokens.multitenancy.Multitenancy; import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.multitenancy.exception.CannotModifyBaseConfigException; -import io.supertokens.multitenancy.exception.DeletionInProgressException; import io.supertokens.pluginInterface.exceptions.InvalidConfigException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.*; @@ -69,7 +68,7 @@ public void beforeEach() { @Test public void testThatTenantCannotSetDatabaseRelatedConfigIfSuperTokensSaaSSecretIsSet() throws InterruptedException, IOException, InvalidConfigException, TenantOrAppNotFoundException, - InvalidProviderConfigException, DeletionInProgressException, StorageQueryException, + InvalidProviderConfigException, StorageQueryException, FeatureNotEnabledException, CannotModifyBaseConfigException { String[] args = {"../"}; @@ -87,11 +86,10 @@ public void testThatTenantCannotSetDatabaseRelatedConfigIfSuperTokensSaaSSecretI try { JsonObject j = new JsonObject(); j.addProperty(PROTECTED_DB_CONFIG[i], ""); - Multitenancy.addNewOrUpdateAppOrTenant(process.main, new TenantIdentifier(null, null, null), - new TenantConfig(new TenantIdentifier(null, null, "t1"), new EmailPasswordConfig(false), - new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), - new PasswordlessConfig(false), - j), true); + Multitenancy.addNewOrUpdateAppOrTenant(process.main, new TenantConfig(new TenantIdentifier(null, null, "t1"), new EmailPasswordConfig(false), + new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), + new PasswordlessConfig(false), + j), true); fail(); } catch (BadPermissionException e) { assertEquals(e.getMessage(), "Not allowed to modify DB related configs."); @@ -110,7 +108,7 @@ public void testThatTenantCannotSetDatabaseRelatedConfigIfSuperTokensSaaSSecretI @Test public void testThatTenantCanSetDatabaseRelatedConfigIfSuperTokensSaaSSecretIsNotSet() throws InterruptedException, IOException, InvalidConfigException, TenantOrAppNotFoundException, - InvalidProviderConfigException, DeletionInProgressException, StorageQueryException, + InvalidProviderConfigException, StorageQueryException, FeatureNotEnabledException, CannotModifyBaseConfigException, BadPermissionException { String[] args = {"../"}; @@ -129,11 +127,11 @@ public void testThatTenantCanSetDatabaseRelatedConfigIfSuperTokensSaaSSecretIsNo } else if (PROTECTED_DB_CONFIG_VALUES[i] instanceof Integer) { j.addProperty(PROTECTED_DB_CONFIG[i], (Integer) PROTECTED_DB_CONFIG_VALUES[i]); } - Multitenancy.addNewOrUpdateAppOrTenant(process.main, new TenantIdentifier(null, null, null), - new TenantConfig(new TenantIdentifier(null, null, "t1"), new EmailPasswordConfig(false), - new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), - new PasswordlessConfig(false), - j), false); + Multitenancy.addNewOrUpdateAppOrTenant(process.main, new TenantConfig(new TenantIdentifier(null, null, "t1"), + new EmailPasswordConfig(false), + new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), + new PasswordlessConfig(false), + j), false); } // TODO: we should call the API to add a new tenant with api key, and check @@ -146,7 +144,7 @@ public void testThatTenantCanSetDatabaseRelatedConfigIfSuperTokensSaaSSecretIsNo @Test public void testThatTenantCannotGetDatabaseRelatedConfigIfSuperTokensSaaSSecretIsSet() throws InterruptedException, IOException, InvalidConfigException, TenantOrAppNotFoundException, - InvalidProviderConfigException, DeletionInProgressException, StorageQueryException, + InvalidProviderConfigException, StorageQueryException, FeatureNotEnabledException, CannotModifyBaseConfigException, BadPermissionException { String[] args = {"../"}; diff --git a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java index 2440a370..797ccbdb 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java @@ -30,7 +30,6 @@ import io.supertokens.multitenancy.MultitenancyHelper; import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.multitenancy.exception.CannotModifyBaseConfigException; -import io.supertokens.multitenancy.exception.DeletionInProgressException; import io.supertokens.pluginInterface.Storage; import io.supertokens.pluginInterface.exceptions.DbInitException; import io.supertokens.pluginInterface.exceptions.InvalidConfigException; @@ -724,7 +723,7 @@ public void testCreating50StorageLayersUsage() public void testCantCreateTenantWithUnknownDb() throws InterruptedException, IOException, InvalidConfigException, TenantOrAppNotFoundException, BadPermissionException, InvalidProviderConfigException, - DeletionInProgressException, FeatureNotEnabledException, + FeatureNotEnabledException, CannotModifyBaseConfigException { String[] args = {"../"}; @@ -812,9 +811,7 @@ public void testTenantCreationAndThenDbDownDbThrowsErrorInRecipesAndDoesntAffect "does not exist"); } - assertEquals(2, - Multitenancy.getAllTenants(new TenantIdentifier(null, null, null), process.getProcess()).length); - + assertEquals(2, Multitenancy.getAllTenants(process.getProcess()).length); process.kill(false); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -829,8 +826,7 @@ public void testTenantCreationAndThenDbDownDbThrowsErrorInRecipesAndDoesntAffect process.startProcess(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - assertEquals(2, - Multitenancy.getAllTenants(new TenantIdentifier(null, null, null), process.getProcess()).length); + assertEquals(2, Multitenancy.getAllTenants(process.getProcess()).length); TenantIdentifier tid = new TenantIdentifier("abc", null, null); try { From 741a9b288ed5bd5f795af6fce62545e0bc51fc51 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Mon, 24 Apr 2023 15:32:15 +0530 Subject: [PATCH 063/106] fix: Misc changes (#89) * fix: session expiry index * fix: active users --- .../supertokens/storage/postgresql/Start.java | 28 ++----------- .../queries/ActiveUsersQueries.java | 40 ++++++++++++------- .../postgresql/queries/GeneralQueries.java | 6 ++- .../postgresql/queries/SessionQueries.java | 5 +++ 4 files changed, 39 insertions(+), 40 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index b4e2bc8b..84fbec9f 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -103,8 +103,6 @@ import java.util.List; import java.util.Set; -import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; - public class Start implements SessionSQLStorage, EmailPasswordSQLStorage, EmailVerificationSQLStorage, ThirdPartySQLStorage, JWTRecipeSQLStorage, PasswordlessSQLStorage, UserMetadataSQLStorage, UserRolesSQLStorage, UserIdMappingStorage, @@ -674,7 +672,6 @@ public boolean canBeUsed(JsonObject configJson) { @Override public boolean isUserIdBeingUsedInNonAuthRecipe(AppIdentifier appIdentifier, String className, String userId) throws StorageQueryException { - // TODO.. // check if the input userId is being used in nonAuthRecipes. if (className.equals(SessionStorage.class.getName())) { String[] sessionHandlesForUser = getAllNonExpiredSessionHandlesForUser(appIdentifier, userId); @@ -1273,19 +1270,10 @@ public boolean doesUserIdExist(AppIdentifier appIdentifier, String userId) throw } } - public void updateLastActive(String userId) throws StorageQueryException { - try { - ActiveUsersQueries.updateUserLastActive(this, userId); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - @Override public void updateLastActive(AppIdentifier appIdentifier, String userId) throws StorageQueryException { try { - // TODO.. - ActiveUsersQueries.updateUserLastActive(this, userId); + ActiveUsersQueries.updateUserLastActive(this, appIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1294,8 +1282,7 @@ public void updateLastActive(AppIdentifier appIdentifier, String userId) throws @Override public int countUsersActiveSince(AppIdentifier appIdentifier, long time) throws StorageQueryException { try { - // TODO... - return ActiveUsersQueries.countUsersActiveSince(this, time); + return ActiveUsersQueries.countUsersActiveSince(this, appIdentifier, time); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1304,8 +1291,7 @@ public int countUsersActiveSince(AppIdentifier appIdentifier, long time) throws @Override public int countUsersEnabledTotp(AppIdentifier appIdentifier) throws StorageQueryException { try { - // TODO... - return ActiveUsersQueries.countUsersEnabledTotp(this); + return ActiveUsersQueries.countUsersEnabledTotp(this, appIdentifier); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1315,8 +1301,7 @@ public int countUsersEnabledTotp(AppIdentifier appIdentifier) throws StorageQuer public int countUsersEnabledTotpAndActiveSince(AppIdentifier appIdentifier, long time) throws StorageQueryException { try { - // TODO... - return ActiveUsersQueries.countUsersEnabledTotpAndActiveSince(this, time); + return ActiveUsersQueries.countUsersEnabledTotpAndActiveSince(this, appIdentifier, time); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -2075,7 +2060,6 @@ public void createUserIdMapping(AppIdentifier appIdentifier, String superTokensU externalUserId, @Nullable String externalUserIdInfo) throws StorageQueryException, UnknownSuperTokensUserIdException, UserIdMappingAlreadyExistsException { - // TODO.. try { UserIdMappingQueries.createUserIdMapping(this, appIdentifier, superTokensUserId, externalUserId, externalUserIdInfo); @@ -2111,7 +2095,6 @@ public void createUserIdMapping(AppIdentifier appIdentifier, String superTokensU public boolean deleteUserIdMapping(AppIdentifier appIdentifier, String userId, boolean isSuperTokensUserId) throws StorageQueryException { - // TODO.. try { if (isSuperTokensUserId) { return UserIdMappingQueries.deleteUserIdMappingWithSuperTokensUserId(this, appIdentifier, @@ -2128,7 +2111,6 @@ public boolean deleteUserIdMapping(AppIdentifier appIdentifier, String userId, public UserIdMapping getUserIdMapping(AppIdentifier appIdentifier, String userId, boolean isSuperTokensUserId) throws StorageQueryException { - // TODO.. try { if (isSuperTokensUserId) { return UserIdMappingQueries.getuseraIdMappingWithSuperTokensUserId(this, appIdentifier, @@ -2144,7 +2126,6 @@ public UserIdMapping getUserIdMapping(AppIdentifier appIdentifier, String userId @Override public UserIdMapping[] getUserIdMapping(AppIdentifier appIdentifier, String userId) throws StorageQueryException { - // TODO.. try { return UserIdMappingQueries.getUserIdMappingWithEitherSuperTokensUserIdOrExternalUserId(this, appIdentifier, @@ -2159,7 +2140,6 @@ public boolean updateOrDeleteExternalUserIdInfo(AppIdentifier appIdentifier, Str boolean isSuperTokensUserId, @Nullable String externalUserIdInfo) throws StorageQueryException { - // TODO.. try { if (isSuperTokensUserId) { return UserIdMappingQueries.updateOrDeleteExternalUserIdInfoWithSuperTokensUserId(this, diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java index 1be94685..b0877928 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java @@ -2,6 +2,7 @@ import java.sql.SQLException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.storage.postgresql.Start; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.storage.postgresql.config.Config; @@ -12,15 +13,19 @@ public class ActiveUsersQueries { static String getQueryToCreateUserLastActiveTable(Start start) { return "CREATE TABLE IF NOT EXISTS " + Config.getConfig(start).getUserLastActiveTable() + " (" + + "app_id VARCHAR(64) DEFAULT 'public'," + "user_id VARCHAR(128)," - + "last_active_time BIGINT," + "PRIMARY KEY(user_id)" + " );"; + + "last_active_time BIGINT," + "PRIMARY KEY(app_id, user_id)" + " );"; } - public static int countUsersActiveSince(Start start, long sinceTime) throws SQLException, StorageQueryException { + public static int countUsersActiveSince(Start start, AppIdentifier appIdentifier, long sinceTime) throws SQLException, StorageQueryException { String QUERY = "SELECT COUNT(*) as total FROM " + Config.getConfig(start).getUserLastActiveTable() - + " WHERE last_active_time >= ?"; + + " WHERE app_id = ? AND last_active_time >= ?"; - return execute(start, QUERY, pst -> pst.setLong(1, sinceTime), result -> { + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setLong(2, sinceTime); + }, result -> { if (result.next()) { return result.getInt("total"); } @@ -29,10 +34,13 @@ public static int countUsersActiveSince(Start start, long sinceTime) throws SQLE } - public static int countUsersEnabledTotp(Start start) throws SQLException, StorageQueryException { - String QUERY = "SELECT COUNT(*) as total FROM " + Config.getConfig(start).getTotpUsersTable(); + 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, null, result -> { + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + }, result -> { if (result.next()) { return result.getInt("total"); } @@ -40,13 +48,16 @@ public static int countUsersEnabledTotp(Start start) throws SQLException, Storag }); } - public static int countUsersEnabledTotpAndActiveSince(Start start, long sinceTime) throws SQLException, StorageQueryException { + 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.last_active_time >= ?"; + + "WHERE user_last_active.app_id = ? AND user_last_active.last_active_time >= ?"; - return execute(start, QUERY, pst -> pst.setLong(1, sinceTime), result -> { + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setLong(2, sinceTime); + }, result -> { if (result.next()) { return result.getInt("total"); } @@ -54,15 +65,16 @@ public static int countUsersEnabledTotpAndActiveSince(Start start, long sinceTim }); } - public static int updateUserLastActive(Start start, String userId) throws SQLException, StorageQueryException { + public static int updateUserLastActive(Start start, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + Config.getConfig(start).getUserLastActiveTable() - + "(user_id, last_active_time) VALUES(?, ?) ON CONFLICT(user_id) DO UPDATE SET last_active_time = ?"; + + "(app_id, user_id, last_active_time) VALUES(?, ?, ?) ON CONFLICT(app_id, user_id) DO UPDATE SET last_active_time = ?"; long now = System.currentTimeMillis(); return update(start, QUERY, pst -> { - pst.setString(1, userId); - pst.setLong(2, now); + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); pst.setLong(3, now); + pst.setLong(4, now); }); } } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java index 9392caf9..121d21f2 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java @@ -51,8 +51,7 @@ import static io.supertokens.storage.postgresql.queries.EmailVerificationQueries.*; import static io.supertokens.storage.postgresql.queries.JWTSigningQueries.getQueryToCreateJWTSigningTable; import static io.supertokens.storage.postgresql.queries.PasswordlessQueries.*; -import static io.supertokens.storage.postgresql.queries.SessionQueries.getQueryToCreateAccessTokenSigningKeysTable; -import static io.supertokens.storage.postgresql.queries.SessionQueries.getQueryToCreateSessionInfoTable; +import static io.supertokens.storage.postgresql.queries.SessionQueries.*; import static io.supertokens.storage.postgresql.queries.UserMetadataQueries.getQueryToCreateUserMetadataTable; public class GeneralQueries { @@ -207,6 +206,9 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto if (!doesTableExists(start, Config.getConfig(start).getSessionInfoTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); update(start, getQueryToCreateSessionInfoTable(start), NO_OP_SETTER); + + // index + update(start, getQueryToCreateSessionExpiryIndex(start), NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getTenantConfigsTable())) { diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/SessionQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/SessionQueries.java index bb42033c..40210f55 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/SessionQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/SessionQueries.java @@ -84,6 +84,11 @@ static String getQueryToCreateAccessTokenSigningKeysTable(Start start) { // @formatter:on } + static String getQueryToCreateSessionExpiryIndex(Start start) { + return "CREATE INDEX session_expiry_index ON " + + Config.getConfig(start).getSessionInfoTable() + "(expires_at);"; + } + public static void createNewSession(Start start, TenantIdentifier tenantIdentifier, String sessionHandle, String userId, String refreshTokenHash2, JsonObject userDataInDatabase, long expiry, JsonObject userDataInJWT, From 6662fb573d957e3fd58c495b0253012c45d04a87 Mon Sep 17 00:00:00 2001 From: KShivendu Date: Thu, 27 Apr 2023 13:21:37 +0530 Subject: [PATCH 064/106] feat: Introduce MFA recipe in postgresql plugin --- .../supertokens/storage/postgresql/Start.java | 43 +++++++++- .../postgresql/config/PostgreSQLConfig.java | 4 + .../postgresql/queries/GeneralQueries.java | 7 +- .../postgresql/queries/MfaQueries.java | 86 +++++++++++++++++++ 4 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 1e6b1b3a..79d16c87 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -47,6 +47,7 @@ import io.supertokens.pluginInterface.jwt.JWTSigningKeyInfo; import io.supertokens.pluginInterface.jwt.exceptions.DuplicateKeyIdException; import io.supertokens.pluginInterface.jwt.sqlstorage.JWTRecipeSQLStorage; +import io.supertokens.pluginInterface.mfa.MfaStorage; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.MultitenancyStorage; import io.supertokens.pluginInterface.multitenancy.TenantConfig; @@ -106,7 +107,7 @@ public class Start implements SessionSQLStorage, EmailPasswordSQLStorage, EmailVerificationSQLStorage, ThirdPartySQLStorage, JWTRecipeSQLStorage, PasswordlessSQLStorage, UserMetadataSQLStorage, UserRolesSQLStorage, UserIdMappingStorage, - MultitenancyStorage, DashboardSQLStorage, TOTPSQLStorage, ActiveUsersStorage { + MultitenancyStorage, DashboardSQLStorage, TOTPSQLStorage, ActiveUsersStorage, MfaStorage { private static final Object appenderLock = new Object(); public static boolean silent = false; @@ -2614,4 +2615,44 @@ public int removeExpiredCodes(TenantIdentifier tenantIdentifier, long expiredBef throw new StorageQueryException(e); } } + + // MFA recipe: + @Override + public boolean enableFactor(TenantIdentifier tenantIdentifier, String userId, String factor) + throws StorageQueryException { + try { + int insertedCount = MfaQueries.enableFactor(this, tenantIdentifier, userId, factor); + if (insertedCount == 0) { + return false; + } + return true; + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public String[] listFactors(TenantIdentifier tenantIdentifier, String userId) + throws StorageQueryException { + try { + return MfaQueries.listFactors(this, tenantIdentifier, userId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public boolean disableFactor(TenantIdentifier tenantIdentifier, String userId, String factor) + throws StorageQueryException { + try { + int deletedCount = MfaQueries.disableFactor(this, tenantIdentifier, userId, factor); + if (deletedCount == 0) { + return false; + } + return true; + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + } diff --git a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java index de7020e0..f67a5407 100644 --- a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java +++ b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java @@ -388,6 +388,10 @@ public String getTotpUsedCodesTable() { return addSchemaAndPrefixToTableName("totp_used_codes"); } + public String getMfaUserFactorsTable() { + return addSchemaAndPrefixToTableName("mfa_user_factors"); + } + private String addSchemaAndPrefixToTableName(String tableName) { String name = tableName; if (!getTablePrefix().equals("")) { diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java index 313c8e15..6fe661c7 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java @@ -365,6 +365,11 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto update(start, TOTPQueries.getQueryToCreateUsedCodesExpiryTimeIndex(start), NO_OP_SETTER); } + if (!doesTableExists(start, Config.getConfig(start).getMfaUserFactorsTable())) { + getInstance(start).addState(CREATING_NEW_TABLE, null); + update(start, MfaQueries.getQueryToCreateUserFactorsTable(start), NO_OP_SETTER); + } + } catch (Exception e) { if (e.getMessage().contains("schema") && e.getMessage().contains("does not exist") && numberOfRetries < 1) { @@ -431,7 +436,7 @@ public static void deleteAllTables(Start start) throws SQLException, StorageQuer + getConfig(start).getDashboardUsersTable() + "," + getConfig(start).getDashboardSessionsTable() + "," + getConfig(start).getTotpUsedCodesTable() + "," + getConfig(start).getTotpUserDevicesTable() + "," - + getConfig(start).getTotpUsersTable(); + + getConfig(start).getTotpUsersTable() + "," + getConfig(start).getMfaUserFactorsTable(); update(start, DROP_QUERY, NO_OP_SETTER); } } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java new file mode 100644 index 00000000..bbf61e85 --- /dev/null +++ b/src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java @@ -0,0 +1,86 @@ +/* + * 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.storage.postgresql.queries; + +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.storage.postgresql.Start; +import io.supertokens.storage.postgresql.config.Config; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; +import static io.supertokens.storage.postgresql.QueryExecutorTemplate.update; + +public class MfaQueries { + public static String getQueryToCreateUserFactorsTable(Start start) { + return "CREATE TABLE IF NOT EXISTS " + Config.getConfig(start).getMfaUserFactorsTable() + " (" + + "app_id VARCHAR(64) DEFAULT 'public'," + + "tenant_id VARCHAR(64) DEFAULT 'public'," + + "user_id VARCHAR(255) NOT NULL," + + "factor_id VARCHAR(255) NOT NULL," + + "PRIMARY KEY (app_id, tenant_id, user_id, factor_id)," + + "FOREIGN KEY (app_id, tenant_id)" + + "REFERENCES " + Config.getConfig(start).getTenantsTable() + " (app_id, tenant_id) ON DELETE CASCADE"; + } + + public static int enableFactor(Start start, TenantIdentifier tenantIdentifier, String userId, String factorId) + throws StorageQueryException, SQLException { + String QUERY = "INSERT INTO " + Config.getConfig(start).getMfaUserFactorsTable() + " (app_id, tenant_id, user_id, factor_id) VALUES (?, ?, ?, ?) ON CONFLICT DO NOTHING"; + + return update(start, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, userId); + pst.setString(4, factorId); + }); + } + + + public static String[] listFactors(Start start, TenantIdentifier tenantIdentifier, String userId) + throws StorageQueryException, SQLException { + String QUERY = "SELECT factor_id FROM " + Config.getConfig(start).getMfaUserFactorsTable() + " WHERE app_id = ? AND tenant_id = ? AND user_id = ?"; + + return execute(start, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, userId); + }, result -> { + List factors = new ArrayList<>(); + while (result.next()) { + factors.add(result.getString("factor_id")); + } + + return factors.toArray(String[]::new); + }); + } + + public static int disableFactor(Start start, TenantIdentifier tenantIdentifier, String userId, String factorId) + throws StorageQueryException, SQLException { + String QUERY = "DELETE FROM " + Config.getConfig(start).getMfaUserFactorsTable() + " WHERE app_id = ? AND tenant_id = ? AND user_id = ? AND factor_id = ?"; + + return update(start, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, userId); + pst.setString(4, factorId); + }); + } + +} From 45e6e09e339f7e25e4618c7fbda2a2b6dc071760 Mon Sep 17 00:00:00 2001 From: KShivendu Date: Thu, 27 Apr 2023 13:23:46 +0530 Subject: [PATCH 065/106] chores: Mention MFA recipe support in CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1b659f6..a70eb7a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +- Support for MFA recipe + ## [3.0.0] - 2023-04-05 - Adds `use_static_key` `BOOLEAN` column into `session_info` From 0e2b0e6b051acbbaf61073e86360b0575a06ffb6 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Thu, 27 Apr 2023 13:48:25 +0530 Subject: [PATCH 066/106] fix: Tenantid in userobjects (#90) * fix: adding tenant ids to user objects * fix: create user type * fix: test fixes * fix: transaction * fix: refactored ep and tp * fix: refactor pless * fix: pr comment * fix: pr comment --- .../supertokens/storage/postgresql/Start.java | 23 ++-- .../queries/EmailPasswordQueries.java | 89 ++++++++++--- .../postgresql/queries/GeneralQueries.java | 44 ++++++- .../queries/PasswordlessQueries.java | 114 +++++++++++++---- .../postgresql/queries/ThirdPartyQueries.java | 120 +++++++++++++----- .../postgresql/test/ExceptionParsingTest.java | 38 ++---- 6 files changed, 318 insertions(+), 110 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 84fbec9f..54d6f9a3 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -787,12 +787,11 @@ public String[] getProtectedConfigsFromSuperTokensSaaSUsers() { } @Override - public void signUp(TenantIdentifier tenantIdentifier, UserInfo userInfo) + public UserInfo signUp(TenantIdentifier tenantIdentifier, String id, String email, String passwordHash, long timeJoined) throws StorageQueryException, DuplicateUserIdException, DuplicateEmailException, TenantOrAppNotFoundException { try { - EmailPasswordQueries.signUp(this, tenantIdentifier, userInfo.id, userInfo.email, userInfo.passwordHash, - userInfo.timeJoined); + return EmailPasswordQueries.signUp(this, tenantIdentifier, id, email, passwordHash, timeJoined); } catch (StorageTransactionLogicException eTemp) { Exception e = eTemp.actualException; if (e instanceof PSQLException) { @@ -1146,12 +1145,13 @@ public void updateUserEmail_Transaction(AppIdentifier appIdentifier, Transaction } @Override - public void signUp(TenantIdentifier tenantIdentifier, io.supertokens.pluginInterface.thirdparty.UserInfo - userInfo) + public io.supertokens.pluginInterface.thirdparty.UserInfo signUp( + TenantIdentifier tenantIdentifier, String id, String email, + io.supertokens.pluginInterface.thirdparty.UserInfo.ThirdParty thirdParty, long timeJoined) throws StorageQueryException, io.supertokens.pluginInterface.thirdparty.exception.DuplicateUserIdException, DuplicateThirdPartyUserException, TenantOrAppNotFoundException { try { - ThirdPartyQueries.signUp(this, tenantIdentifier, userInfo); + return ThirdPartyQueries.signUp(this, tenantIdentifier, id, email, thirdParty, timeJoined); } catch (StorageTransactionLogicException eTemp) { Exception e = eTemp.actualException; if (e instanceof PSQLException) { @@ -1619,13 +1619,18 @@ public void createCode(TenantIdentifier tenantIdentifier, PasswordlessCode code) } @Override - public void createUser(TenantIdentifier - tenantIdentifier, io.supertokens.pluginInterface.passwordless.UserInfo user) + public io.supertokens.pluginInterface.passwordless.UserInfo createUser(TenantIdentifier tenantIdentifier, + String id, @javax.annotation.Nullable String email, + @javax.annotation.Nullable String phoneNumber, long timeJoined) throws StorageQueryException, DuplicateEmailException, DuplicatePhoneNumberException, DuplicateUserIdException, TenantOrAppNotFoundException { + if (email == null && phoneNumber == null) { + throw new IllegalArgumentException("Both email and phoneNumber cannot be null"); + } + try { - PasswordlessQueries.createUser(this, tenantIdentifier, user); + return PasswordlessQueries.createUser(this, tenantIdentifier, id, email, phoneNumber, timeJoined); } catch (StorageTransactionLogicException e) { Exception actualException = e.actualException; diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java index 03b90c50..bf065fe2 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java @@ -23,6 +23,7 @@ import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.storage.postgresql.ConnectionPool; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.config.Config; import io.supertokens.storage.postgresql.utils.Utils; @@ -30,9 +31,7 @@ import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; +import java.util.*; import static io.supertokens.pluginInterface.RECIPE_ID.EMAIL_PASSWORD; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; @@ -209,7 +208,7 @@ public static UserInfo getUserInfoUsingId_Transaction(Start start, Connection co String QUERY = "SELECT user_id, email, password_hash, time_joined FROM " + getConfig(start).getEmailPasswordUsersTable() + " WHERE app_id = ? AND user_id = ? FOR UPDATE"; - return execute(con, QUERY, pst -> { + UserInfoPartial userInfo = execute(con, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); pst.setString(2, id); }, result -> { @@ -218,6 +217,7 @@ public static UserInfo getUserInfoUsingId_Transaction(Start start, Connection co } return null; }); + return userInfoWithTenantIds_transaction(start, con, userInfo); } public static PasswordResetTokenInfo getPasswordResetTokenInfo(Start start, AppIdentifier appIdentifier, String token) @@ -248,9 +248,9 @@ public static void addPasswordResetToken(Start start, AppIdentifier appIdentifie }); } - public static void signUp(Start start, TenantIdentifier tenantIdentifier, String userId, String email, String passwordHash, long timeJoined) + public static UserInfo signUp(Start start, TenantIdentifier tenantIdentifier, String userId, String email, String passwordHash, long timeJoined) throws StorageQueryException, StorageTransactionLogicException { - start.startTransaction(con -> { + return start.startTransaction(con -> { Connection sqlCon = (Connection) con.getConnection(); try { { // app_id_to_user_id @@ -300,11 +300,13 @@ public static void signUp(Start start, TenantIdentifier tenantIdentifier, String }); } + UserInfo userInfo = userInfoWithTenantIds_transaction(start, sqlCon, new UserInfoPartial(userId, email, passwordHash, timeJoined)); + sqlCon.commit(); + return userInfo; } catch (SQLException throwables) { throw new StorageTransactionLogicException(throwables); } - return null; }); } @@ -335,7 +337,7 @@ public static UserInfo getUserInfoUsingId(Start start, AppIdentifier appIdentifi String QUERY = "SELECT user_id, email, password_hash, time_joined FROM " + getConfig(start).getEmailPasswordUsersTable() + " WHERE app_id = ? AND user_id = ?"; - return execute(start, QUERY.toString(), pst -> { + UserInfoPartial userInfo = execute(start, QUERY.toString(), pst -> { pst.setString(1, appIdentifier.getAppId()); pst.setString(2, id); }, result -> { @@ -344,9 +346,10 @@ public static UserInfo getUserInfoUsingId(Start start, AppIdentifier appIdentifi } return null; }); + return userInfoWithTenantIds(start, userInfo); } - public static UserInfo getUserInfoUsingId(Start start, Connection sqlCon, AppIdentifier appIdentifier, String id) throws SQLException, StorageQueryException { + public static UserInfoPartial getUserInfoUsingId(Start start, Connection sqlCon, AppIdentifier appIdentifier, String id) throws SQLException, StorageQueryException { // we don't need a FOR UPDATE here because this is already part of a transaction, and locked on app_id_to_user_id table String QUERY = "SELECT user_id, email, password_hash, time_joined FROM " + getConfig(start).getEmailPasswordUsersTable() + " WHERE app_id = ? AND user_id = ?"; @@ -379,18 +382,19 @@ public static List getUsersInfoUsingIdList(Start start, List i } QUERY.append(")"); - return execute(start, QUERY.toString(), pst -> { + List userInfos = execute(start, QUERY.toString(), pst -> { for (int i = 0; i < ids.size(); i++) { // i+1 cause this starts with 1 and not 0 pst.setString(i + 1, ids.get(i)); } }, result -> { - List finalResult = new ArrayList<>(); + List finalResult = new ArrayList<>(); while (result.next()) { finalResult.add(UserInfoRowMapper.getInstance().mapOrThrow(result)); } return finalResult; }); + return userInfoWithTenantIds(start, userInfos); } return Collections.emptyList(); } @@ -403,7 +407,7 @@ public static UserInfo getUserInfoUsingEmail(Start start, TenantIdentifier tenan + "ON ep_users.app_id = ep_users_to_tenant.app_id AND ep_users.user_id = ep_users_to_tenant.user_id " + "WHERE ep_users_to_tenant.app_id = ? AND ep_users_to_tenant.tenant_id = ? AND ep_users_to_tenant.email = ?"; - return execute(start, QUERY, pst -> { + UserInfoPartial userInfo = execute(start, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, tenantIdentifier.getTenantId()); pst.setString(3, email); @@ -413,12 +417,13 @@ public static UserInfo getUserInfoUsingEmail(Start start, TenantIdentifier tenan } return null; }); + return userInfoWithTenantIds(start, userInfo); } public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId) throws SQLException, StorageQueryException { - UserInfo userInfo = EmailPasswordQueries.getUserInfoUsingId(start, sqlCon, + UserInfoPartial userInfo = EmailPasswordQueries.getUserInfoUsingId(start, sqlCon, tenantIdentifier.toAppIdentifier(), userId); { // all_auth_recipe_users @@ -466,6 +471,58 @@ public static boolean removeUserIdFromTenant_Transaction(Start start, Connection // automatically deleted from emailpassword_user_to_tenant because of foreign key constraint } + private static UserInfo userInfoWithTenantIds(Start start, UserInfoPartial userInfo) + throws SQLException, StorageQueryException { + if (userInfo == null) return null; + try (Connection con = ConnectionPool.getConnection(start)) { + return userInfoWithTenantIds_transaction(start, con, Arrays.asList(userInfo)).get(0); + } + } + + private static List userInfoWithTenantIds(Start start, List userInfos) + throws SQLException, StorageQueryException { + try (Connection con = ConnectionPool.getConnection(start)) { + return userInfoWithTenantIds_transaction(start, con, userInfos); + } + } + + private static UserInfo userInfoWithTenantIds_transaction(Start start, Connection sqlCon, UserInfoPartial userInfo) + throws SQLException, StorageQueryException { + if (userInfo == null) return null; + return userInfoWithTenantIds_transaction(start, sqlCon, Arrays.asList(userInfo)).get(0); + } + + private static List userInfoWithTenantIds_transaction(Start start, Connection sqlCon, List userInfos) + throws SQLException, StorageQueryException { + String[] userIds = new String[userInfos.size()]; + for (int i = 0; i < userInfos.size(); i++) { + userIds[i] = userInfos.get(i).id; + } + + Map> tenantIdsForUserIds = GeneralQueries.getTenantIdsForUserIds_transaction(start, sqlCon, userIds); + List result = new ArrayList<>(); + for (UserInfoPartial userInfo : userInfos) { + result.add(new UserInfo(userInfo.id, userInfo.email, userInfo.passwordHash, userInfo.timeJoined, + tenantIdsForUserIds.get(userInfo.id).toArray(new String[0]))); + } + + return result; + } + + private static class UserInfoPartial { + public final String id; + public final long timeJoined; + public final String email; + public final String passwordHash; + + public UserInfoPartial(String id, String email, String passwordHash, long timeJoined) { + this.id = id; + this.timeJoined = timeJoined; + this.email = email; + this.passwordHash = passwordHash; + } + } + private static class PasswordResetRowMapper implements RowMapper { public static final PasswordResetRowMapper INSTANCE = new PasswordResetRowMapper(); @@ -487,7 +544,7 @@ public PasswordResetTokenInfo map(ResultSet result) throws StorageQueryException } } - private static class UserInfoRowMapper implements RowMapper { + private static class UserInfoRowMapper implements RowMapper { static final UserInfoRowMapper INSTANCE = new UserInfoRowMapper(); private UserInfoRowMapper() { @@ -498,8 +555,8 @@ private static UserInfoRowMapper getInstance() { } @Override - public UserInfo map(ResultSet result) throws Exception { - return new UserInfo(result.getString("user_id"), result.getString("email"), + public UserInfoPartial map(ResultSet result) throws Exception { + return new UserInfoPartial(result.getString("user_id"), result.getString("email"), result.getString("password_hash"), result.getLong("time_joined")); } } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java index 121d21f2..c40d7b73 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java @@ -35,10 +35,7 @@ import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import static io.supertokens.storage.postgresql.PreparedStatementValueSetter.NO_OP_SETTER; import static io.supertokens.storage.postgresql.ProcessState.PROCESS_STATE.CREATING_NEW_TABLE; @@ -945,6 +942,45 @@ public static String getRecipeIdForUser_Transaction(Start start, Connection sqlC }); } + public static Map> getTenantIdsForUserIds_transaction(Start start, Connection sqlCon, String[] userIds) + throws SQLException, StorageQueryException { + if (userIds != null && userIds.length > 0) { + StringBuilder QUERY = new StringBuilder("SELECT user_id, tenant_id " + + "FROM " + getConfig(start).getUsersTable()); + QUERY.append(" WHERE user_id IN ("); + for (int i = 0; i < userIds.length; i++) { + + QUERY.append("?"); + if (i != userIds.length - 1) { + // not the last element + QUERY.append(","); + } + } + QUERY.append(")"); + + return execute(sqlCon, QUERY.toString(), pst -> { + for (int i = 0; i < userIds.length; i++) { + // i+1 cause this starts with 1 and not 0 + pst.setString(i + 1, userIds[i]); + } + }, result -> { + Map> finalResult = new HashMap<>(); + while (result.next()) { + String userId = result.getString("user_id"); + String tenantId = result.getString("tenant_id"); + + if (!finalResult.containsKey(userId)) { + finalResult.put(userId, new ArrayList<>()); + } + finalResult.get(userId).add(tenantId); + } + return finalResult; + }); + } + + return new HashMap<>(); + } + private static class UserInfoPaginationResultHolder { String userId; String recipeId; diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java index 8dddd1c5..4b133772 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java @@ -31,12 +31,11 @@ import io.supertokens.storage.postgresql.utils.Utils; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; +import java.util.*; import static io.supertokens.pluginInterface.RECIPE_ID.PASSWORDLESS; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; @@ -362,9 +361,9 @@ public static void deleteCode_Transaction(Start start, Connection con, TenantIde }); } - public static void createUser(Start start, TenantIdentifier tenantIdentifier, UserInfo user) + public static UserInfo createUser(Start start, TenantIdentifier tenantIdentifier, String id, @Nullable String email, @Nullable String phoneNumber, long timeJoined) throws StorageTransactionLogicException, StorageQueryException { - start.startTransaction(con -> { + return start.startTransaction(con -> { Connection sqlCon = (Connection) con.getConnection(); try { { // app_id_to_user_id @@ -372,7 +371,7 @@ public static void createUser(Start start, TenantIdentifier tenantIdentifier, Us + "(app_id, user_id, recipe_id)" + " VALUES(?, ?, ?)"; update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); - pst.setString(2, user.id); + pst.setString(2, id); pst.setString(3, PASSWORDLESS.toString()); }); } @@ -383,9 +382,9 @@ public static void createUser(Start start, TenantIdentifier tenantIdentifier, Us update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, tenantIdentifier.getTenantId()); - pst.setString(3, user.id); + pst.setString(3, id); pst.setString(4, PASSWORDLESS.toString()); - pst.setLong(5, user.timeJoined); + pst.setLong(5, timeJoined); }); } @@ -394,10 +393,10 @@ public static void createUser(Start start, TenantIdentifier tenantIdentifier, Us + "(app_id, user_id, email, phone_number, time_joined)" + " VALUES(?, ?, ?, ?, ?)"; update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); - pst.setString(2, user.id); - pst.setString(3, user.email); - pst.setString(4, user.phoneNumber); - pst.setLong(5, user.timeJoined); + pst.setString(2, id); + pst.setString(3, email); + pst.setString(4, phoneNumber); + pst.setLong(5, timeJoined); }); } @@ -408,16 +407,17 @@ public static void createUser(Start start, TenantIdentifier tenantIdentifier, Us update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, tenantIdentifier.getTenantId()); - pst.setString(3, user.id); - pst.setString(4, user.email); - pst.setString(5, user.phoneNumber); + pst.setString(3, id); + pst.setString(4, email); + pst.setString(5, phoneNumber); }); } + UserInfo userInfo = userInfoWithTenantIds_transaction(start, sqlCon, new UserInfoPartial(id, email, phoneNumber, timeJoined)); sqlCon.commit(); + return userInfo; } catch (SQLException throwables) { throw new StorageTransactionLogicException(throwables); } - return null; }); } @@ -675,18 +675,19 @@ public static List getUsersByIdList(Start start, List ids) } QUERY.append(")"); - return execute(start, QUERY.toString(), pst -> { + List userInfos = execute(start, QUERY.toString(), pst -> { for (int i = 0; i < ids.size(); i++) { // i+1 cause this starts with 1 and not 0 pst.setString(i + 1, ids.get(i)); } }, result -> { - List finalResult = new ArrayList<>(); + List finalResult = new ArrayList<>(); while (result.next()) { finalResult.add(UserInfoRowMapper.getInstance().mapOrThrow(result)); } return finalResult; }); + return userInfoWithTenantIds(start, userInfos); } return Collections.emptyList(); } @@ -695,7 +696,7 @@ public static UserInfo getUserById(Start start, AppIdentifier appIdentifier, Str String QUERY = "SELECT user_id, email, phone_number, time_joined FROM " + getConfig(start).getPasswordlessUsersTable() + " WHERE app_id = ? AND user_id = ?"; - return execute(start, QUERY, pst -> { + UserInfoPartial userInfo = execute(start, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); pst.setString(2, userId); }, result -> { @@ -704,9 +705,10 @@ public static UserInfo getUserById(Start start, AppIdentifier appIdentifier, Str } return null; }); + return userInfoWithTenantIds(start, userInfo); } - public static UserInfo getUserById(Start start, Connection sqlCon, AppIdentifier appIdentifier, String userId) throws StorageQueryException, SQLException { + public static UserInfoPartial getUserById(Start start, Connection sqlCon, AppIdentifier appIdentifier, String userId) throws StorageQueryException, SQLException { // we don't need a FOR UPDATE here because this is already part of a transaction, and locked on app_id_to_user_id table String QUERY = "SELECT user_id, email, phone_number, time_joined FROM " + getConfig(start).getPasswordlessUsersTable() @@ -732,7 +734,7 @@ public static UserInfo getUserByEmail(Start start, TenantIdentifier tenantIdenti + "ON pl_users.app_id = pl_users_to_tenant.app_id AND pl_users.user_id = pl_users_to_tenant.user_id " + "WHERE pl_users_to_tenant.app_id = ? AND pl_users_to_tenant.tenant_id = ? AND pl_users_to_tenant.email = ? "; - return execute(start, QUERY, pst -> { + UserInfoPartial userInfo = execute(start, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, tenantIdentifier.getTenantId()); pst.setString(3, email); @@ -742,6 +744,7 @@ public static UserInfo getUserByEmail(Start start, TenantIdentifier tenantIdenti } return null; }); + return userInfoWithTenantIds(start, userInfo); } public static UserInfo getUserByPhoneNumber(Start start, TenantIdentifier tenantIdentifier, @Nonnull String phoneNumber) @@ -753,7 +756,7 @@ public static UserInfo getUserByPhoneNumber(Start start, TenantIdentifier tenant + "ON pl_users.app_id = pl_users_to_tenant.app_id AND pl_users.user_id = pl_users_to_tenant.user_id " + "WHERE pl_users_to_tenant.app_id = ? AND pl_users_to_tenant.tenant_id = ? AND pl_users_to_tenant.phone_number = ? "; - return execute(start, QUERY, pst -> { + UserInfoPartial userInfo = execute(start, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, tenantIdentifier.getTenantId()); pst.setString(3, phoneNumber); @@ -763,11 +766,12 @@ public static UserInfo getUserByPhoneNumber(Start start, TenantIdentifier tenant } return null; }); + return userInfoWithTenantIds(start, userInfo); } public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId) throws StorageQueryException, SQLException { - UserInfo userInfo = PasswordlessQueries.getUserById(start, sqlCon, + UserInfoPartial userInfo = PasswordlessQueries.getUserById(start, sqlCon, tenantIdentifier.toAppIdentifier(), userId); { // all_auth_recipe_users @@ -818,6 +822,44 @@ public static boolean removeUserIdFromTenant_Transaction(Start start, Connection // automatically deleted from passwordless_user_to_tenant because of foreign key constraint } + private static UserInfo userInfoWithTenantIds(Start start, UserInfoPartial userInfo) + throws SQLException, StorageQueryException { + if (userInfo == null) return null; + try (Connection con = ConnectionPool.getConnection(start)) { + return userInfoWithTenantIds_transaction(start, con, Arrays.asList(userInfo)).get(0); + } + } + + private static List userInfoWithTenantIds(Start start, List userInfos) + throws SQLException, StorageQueryException { + try (Connection con = ConnectionPool.getConnection(start)) { + return userInfoWithTenantIds_transaction(start, con, userInfos); + } + } + + private static UserInfo userInfoWithTenantIds_transaction(Start start, Connection sqlCon, UserInfoPartial userInfo) + throws SQLException, StorageQueryException { + if (userInfo == null) return null; + return userInfoWithTenantIds_transaction(start, sqlCon, Arrays.asList(userInfo)).get(0); + } + + private static List userInfoWithTenantIds_transaction(Start start, Connection sqlCon, List userInfos) + throws SQLException, StorageQueryException { + String[] userIds = new String[userInfos.size()]; + for (int i = 0; i < userInfos.size(); i++) { + userIds[i] = userInfos.get(i).id; + } + + Map> tenantIdsForUserIds = GeneralQueries.getTenantIdsForUserIds_transaction(start, sqlCon, userIds); + List result = new ArrayList<>(); + for (UserInfoPartial userInfo : userInfos) { + result.add(new UserInfo(userInfo.id, userInfo.email, userInfo.phoneNumber, userInfo.timeJoined, + tenantIdsForUserIds.get(userInfo.id).toArray(new String[0]))); + } + + return result; + } + private static class PasswordlessDeviceRowMapper implements RowMapper { private static final PasswordlessDeviceRowMapper INSTANCE = new PasswordlessDeviceRowMapper(); @@ -853,7 +895,26 @@ public PasswordlessCode map(ResultSet result) throws Exception { } } - private static class UserInfoRowMapper implements RowMapper { + private static class UserInfoPartial { + public final String id; + public final long timeJoined; + public final String email; + public final String phoneNumber; + + UserInfoPartial(String id, @Nullable String email, @Nullable String phoneNumber, long timeJoined) { + this.id = id; + this.timeJoined = timeJoined; + + if (email == null && phoneNumber == null) { + throw new IllegalArgumentException("Both email and phoneNumber cannot be null"); + } + + this.email = email; + this.phoneNumber = phoneNumber; + } + } + + private static class UserInfoRowMapper implements RowMapper { private static final UserInfoRowMapper INSTANCE = new UserInfoRowMapper(); private UserInfoRowMapper() { @@ -864,12 +925,13 @@ private static UserInfoRowMapper getInstance() { } @Override - public UserInfo map(ResultSet result) throws Exception { - return new UserInfo(result.getString("user_id"), result.getString("email"), + public UserInfoPartial map(ResultSet result) throws Exception { + return new UserInfoPartial(result.getString("user_id"), result.getString("email"), result.getString("phone_number"), result.getLong("time_joined")); } } + private static class UserInfoWithTenantId { public final String userId; public final String tenantId; diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/ThirdPartyQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/ThirdPartyQueries.java index f5a3f9c4..10fdbc37 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/ThirdPartyQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/ThirdPartyQueries.java @@ -22,6 +22,7 @@ import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.thirdparty.UserInfo; +import io.supertokens.storage.postgresql.ConnectionPool; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.config.Config; import io.supertokens.storage.postgresql.utils.Utils; @@ -30,9 +31,7 @@ import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; +import java.util.*; import static io.supertokens.pluginInterface.RECIPE_ID.THIRD_PARTY; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; @@ -92,9 +91,9 @@ static String getQueryToCreateThirdPartyUserToTenantTable(Start start) { // @formatter:on } - public static void signUp(Start start, TenantIdentifier tenantIdentifier, UserInfo userInfo) + public static UserInfo signUp(Start start, TenantIdentifier tenantIdentifier, String id, String email, UserInfo.ThirdParty thirdParty, long timeJoined) throws StorageQueryException, StorageTransactionLogicException { - start.startTransaction(con -> { + return start.startTransaction(con -> { Connection sqlCon = (Connection) con.getConnection(); try { { // app_id_to_user_id @@ -102,7 +101,7 @@ public static void signUp(Start start, TenantIdentifier tenantIdentifier, UserIn + "(app_id, user_id, recipe_id)" + " VALUES(?, ?, ?)"; update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); - pst.setString(2, userInfo.id); + pst.setString(2, id); pst.setString(3, THIRD_PARTY.toString()); }); } @@ -113,9 +112,9 @@ public static void signUp(Start start, TenantIdentifier tenantIdentifier, UserIn update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, tenantIdentifier.getTenantId()); - pst.setString(3, userInfo.id); + pst.setString(3, id); pst.setString(4, THIRD_PARTY.toString()); - pst.setLong(5, userInfo.timeJoined); + pst.setLong(5, timeJoined); }); } @@ -125,11 +124,11 @@ public static void signUp(Start start, TenantIdentifier tenantIdentifier, UserIn + " VALUES(?, ?, ?, ?, ?, ?)"; update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); - pst.setString(2, userInfo.thirdParty.id); - pst.setString(3, userInfo.thirdParty.userId); - pst.setString(4, userInfo.id); - pst.setString(5, userInfo.email); - pst.setLong(6, userInfo.timeJoined); + pst.setString(2, thirdParty.id); + pst.setString(3, thirdParty.userId); + pst.setString(4, id); + pst.setString(5, email); + pst.setLong(6, timeJoined); }); } @@ -140,17 +139,19 @@ public static void signUp(Start start, TenantIdentifier tenantIdentifier, UserIn update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, tenantIdentifier.getTenantId()); - pst.setString(3, userInfo.id); - pst.setString(4, userInfo.thirdParty.id); - pst.setString(5, userInfo.thirdParty.userId); + pst.setString(3, id); + pst.setString(4, thirdParty.id); + pst.setString(5, thirdParty.userId); }); } + UserInfo userInfo = userInfoWithTenantIds_transaction(start, sqlCon, new UserInfoPartial(id, email, thirdParty, timeJoined)); sqlCon.commit(); + return userInfo; + } catch (SQLException throwables) { throw new StorageTransactionLogicException(throwables); } - return null; }); } @@ -182,7 +183,7 @@ public static UserInfo getThirdPartyUserInfoUsingId(Start start, AppIdentifier a String QUERY = "SELECT user_id, third_party_id, third_party_user_id, email, time_joined FROM " + getConfig(start).getThirdPartyUsersTable() + " WHERE app_id = ? AND user_id = ?"; - return execute(start, QUERY.toString(), pst -> { + UserInfoPartial userInfo = execute(start, QUERY.toString(), pst -> { pst.setString(1, appIdentifier.getAppId()); pst.setString(2, userId); }, result -> { @@ -191,6 +192,7 @@ public static UserInfo getThirdPartyUserInfoUsingId(Start start, AppIdentifier a } return null; }); + return userInfoWithTenantIds(start, userInfo); } public static List getUsersInfoUsingIdList(Start start, List ids) @@ -211,18 +213,19 @@ public static List getUsersInfoUsingIdList(Start start, List i } QUERY.append(")"); - return execute(start, QUERY.toString(), pst -> { + List userInfos = execute(start, QUERY.toString(), pst -> { for (int i = 0; i < ids.size(); i++) { // i+1 cause this starts with 1 and not 0 pst.setString(i + 1, ids.get(i)); } }, result -> { - List finalResult = new ArrayList<>(); + List finalResult = new ArrayList<>(); while (result.next()) { finalResult.add(UserInfoRowMapper.getInstance().mapOrThrow(result)); } return finalResult; }); + return userInfoWithTenantIds(start, userInfos); } return Collections.emptyList(); } @@ -240,7 +243,7 @@ public static UserInfo getThirdPartyUserInfoUsingId(Start start, TenantIdentifie + "WHERE tp_users_to_tenant.app_id = ? AND tp_users_to_tenant.tenant_id = ? " + "AND tp_users_to_tenant.third_party_id = ? AND tp_users_to_tenant.third_party_user_id = ?"; - return execute(start, QUERY, pst -> { + UserInfoPartial userInfo = execute(start, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, tenantIdentifier.getTenantId()); pst.setString(3, thirdPartyId); @@ -251,6 +254,7 @@ public static UserInfo getThirdPartyUserInfoUsingId(Start start, TenantIdentifie } return null; }); + return userInfoWithTenantIds(start, userInfo); } public static void updateUserEmail_Transaction(Start start, Connection con, AppIdentifier appIdentifier, @@ -275,7 +279,7 @@ public static UserInfo getUserInfoUsingId_Transaction(Start start, Connection co String QUERY = "SELECT user_id, third_party_id, third_party_user_id, email, time_joined FROM " + getConfig(start).getThirdPartyUsersTable() + " WHERE app_id = ? AND third_party_id = ? AND third_party_user_id = ? FOR UPDATE"; - return execute(con, QUERY, pst -> { + UserInfoPartial userInfo = execute(con, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); pst.setString(2, thirdPartyId); pst.setString(3, thirdPartyUserId); @@ -285,10 +289,11 @@ public static UserInfo getUserInfoUsingId_Transaction(Start start, Connection co } return null; }); + return userInfoWithTenantIds_transaction(start, con, userInfo); } - public static UserInfo getUserInfoUsingUserId(Start start, Connection con, - AppIdentifier appIdentifier, String userId) + private static UserInfoPartial getUserInfoUsingUserId(Start start, Connection con, + AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { // we don't need a FOR UPDATE here because this is already part of a transaction, and locked on app_id_to_user_id table @@ -319,22 +324,23 @@ public static UserInfo[] getThirdPartyUsersByEmail(Start start, TenantIdentifier + "WHERE tp_users_to_tenant.app_id = ? AND tp_users_to_tenant.tenant_id = ? AND tp_users.email = ? " + "ORDER BY time_joined"; - return execute(start, QUERY.toString(), pst -> { + List userInfos = execute(start, QUERY.toString(), pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, tenantIdentifier.getTenantId()); pst.setString(3, email); }, result -> { - List finalResult = new ArrayList<>(); + List finalResult = new ArrayList<>(); while (result.next()) { finalResult.add(UserInfoRowMapper.getInstance().mapOrThrow(result)); } - return finalResult.toArray(new UserInfo[0]); + return finalResult; }); + return userInfoWithTenantIds(start, userInfos).toArray(new UserInfo[0]); } public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId) throws SQLException, StorageQueryException { - UserInfo userInfo = ThirdPartyQueries.getUserInfoUsingUserId(start, sqlCon, + UserInfoPartial userInfo = ThirdPartyQueries.getUserInfoUsingUserId(start, sqlCon, tenantIdentifier.toAppIdentifier(), userId); { // all_auth_recipe_users @@ -384,7 +390,59 @@ public static boolean removeUserIdFromTenant_Transaction(Start start, Connection // automatically deleted from thirdparty_user_to_tenant because of foreign key constraint } - private static class UserInfoRowMapper implements RowMapper { + private static UserInfo userInfoWithTenantIds(Start start, UserInfoPartial userInfo) + throws SQLException, StorageQueryException { + if (userInfo == null) return null; + try (Connection con = ConnectionPool.getConnection(start)) { + return userInfoWithTenantIds_transaction(start, con, Arrays.asList(userInfo)).get(0); + } + } + + private static List userInfoWithTenantIds(Start start, List userInfos) + throws SQLException, StorageQueryException { + try (Connection con = ConnectionPool.getConnection(start)) { + return userInfoWithTenantIds_transaction(start, con, userInfos); + } + } + + private static UserInfo userInfoWithTenantIds_transaction(Start start, Connection sqlCon, UserInfoPartial userInfo) + throws SQLException, StorageQueryException { + if (userInfo == null) return null; + return userInfoWithTenantIds_transaction(start, sqlCon, Arrays.asList(userInfo)).get(0); + } + + private static List userInfoWithTenantIds_transaction(Start start, Connection sqlCon, List userInfos) + throws SQLException, StorageQueryException { + String[] userIds = new String[userInfos.size()]; + for (int i = 0; i < userInfos.size(); i++) { + userIds[i] = userInfos.get(i).id; + } + + Map> tenantIdsForUserIds = GeneralQueries.getTenantIdsForUserIds_transaction(start, sqlCon, userIds); + List result = new ArrayList<>(); + for (UserInfoPartial userInfo : userInfos) { + result.add(new UserInfo(userInfo.id, userInfo.email, userInfo.thirdParty, userInfo.timeJoined, + tenantIdsForUserIds.get(userInfo.id).toArray(new String[0]))); + } + + return result; + } + + private static class UserInfoPartial { + public final String id; + public final String email; + public final UserInfo.ThirdParty thirdParty; + public final long timeJoined; + + public UserInfoPartial(String id, String email, UserInfo.ThirdParty thirdParty, long timeJoined) { + this.id = id; + this.email = email; + this.thirdParty = thirdParty; + this.timeJoined = timeJoined; + } + } + + private static class UserInfoRowMapper implements RowMapper { private static final UserInfoRowMapper INSTANCE = new UserInfoRowMapper(); private UserInfoRowMapper() { @@ -395,8 +453,8 @@ private static UserInfoRowMapper getInstance() { } @Override - public UserInfo map(ResultSet result) throws Exception { - return new UserInfo(result.getString("user_id"), result.getString("email"), + public UserInfoPartial map(ResultSet result) throws Exception { + return new UserInfoPartial(result.getString("user_id"), result.getString("email"), new UserInfo.ThirdParty(result.getString("third_party_id"), result.getString("third_party_user_id")), result.getLong("time_joined")); diff --git a/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java b/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java index 8afa705f..fb5130a8 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java @@ -20,7 +20,6 @@ import io.supertokens.ProcessState; import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.emailpassword.PasswordResetTokenInfo; -import io.supertokens.pluginInterface.emailpassword.UserInfo; import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicatePasswordResetTokenException; import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateUserIdException; @@ -89,20 +88,19 @@ public void thirdPartySignupExceptions() throws Exception { String userEmail = "useremail@asdf.fdas"; var tp = new io.supertokens.pluginInterface.thirdparty.UserInfo.ThirdParty(tpId, thirdPartyUserId); - var info = new io.supertokens.pluginInterface.thirdparty.UserInfo(userId, userEmail, tp, + storage.signUp(new TenantIdentifier(null, null, null), userId, userEmail, tp, System.currentTimeMillis()); - storage.signUp(new TenantIdentifier(null, null, null), info); try { - storage.signUp(new TenantIdentifier(null, null, null), info); + storage.signUp(new TenantIdentifier(null, null, null), userId, userEmail, tp, + System.currentTimeMillis()); throw new Exception("This should throw"); } catch (io.supertokens.pluginInterface.thirdparty.exception.DuplicateUserIdException ex) { // expected } - var info2 = new io.supertokens.pluginInterface.thirdparty.UserInfo(userId2, userEmail, tp, - System.currentTimeMillis()); try { - storage.signUp(new TenantIdentifier(null, null, null), info2); + storage.signUp(new TenantIdentifier(null, null, null), userId2, userEmail, tp, + System.currentTimeMillis()); throw new Exception("This should throw"); } catch (DuplicateThirdPartyUserException ex) { // expected @@ -130,18 +128,15 @@ public void emailPasswordSignupExceptions() throws Exception { String pwHash = "fakehash"; String userEmail = "useremail@asdf.fdas"; - var info = new UserInfo(userId, userEmail, pwHash, System.currentTimeMillis()); - storage.signUp(new TenantIdentifier(null, null, null), info); + storage.signUp(new TenantIdentifier(null, null, null), userId, userEmail, pwHash, System.currentTimeMillis()); try { - storage.signUp(new TenantIdentifier(null, null, null), info); + storage.signUp(new TenantIdentifier(null, null, null), userId, userEmail, pwHash, System.currentTimeMillis()); throw new Exception("This should throw"); } catch (DuplicateUserIdException ex) { // expected } - var info2 = new UserInfo(userId2, userEmail, pwHash, System.currentTimeMillis()); - try { - storage.signUp(new TenantIdentifier(null, null, null), info2); + storage.signUp(new TenantIdentifier(null, null, null), userId2, userEmail, pwHash, System.currentTimeMillis()); throw new Exception("This should throw"); } catch (DuplicateEmailException ex) { // expected @@ -176,10 +171,8 @@ public void updateUsersEmail_TransactionExceptions() String userEmail2 = "useremail2@asdf.fdas"; String userEmail3 = "useremail3@asdf.fdas"; - var info = new UserInfo(userId, userEmail, pwHash, System.currentTimeMillis()); - var info2 = new UserInfo(userId2, userEmail2, pwHash, System.currentTimeMillis()); - storage.signUp(new TenantIdentifier(null, null, null), info); - storage.signUp(new TenantIdentifier(null, null, null), info2); + storage.signUp(new TenantIdentifier(null, null, null), userId, userEmail, pwHash, System.currentTimeMillis()); + storage.signUp(new TenantIdentifier(null, null, null), userId2, userEmail2, pwHash, System.currentTimeMillis()); storage.startTransaction(conn -> { try { storage.updateUsersEmail_Transaction(new AppIdentifier(null, null), conn, userId, userEmail2); @@ -287,12 +280,11 @@ public void addPasswordResetTokenExceptions() throws Exception { String pwHash = "fakehash"; String userEmail = "useremail@asdf.fdas"; - var userInfo = new UserInfo(userId, userEmail, pwHash, System.currentTimeMillis()); var info = new PasswordResetTokenInfo(userId, tokenHash, System.currentTimeMillis() + 10000); try { storage.addPasswordResetToken(new AppIdentifier(null, null), info); } catch (UnknownUserIdException ex) { - storage.signUp(new TenantIdentifier(null, null, null), userInfo); + storage.signUp(new TenantIdentifier(null, null, null), userId, userEmail, pwHash, System.currentTimeMillis()); } storage.addPasswordResetToken(new AppIdentifier(null, null), info); try { @@ -348,18 +340,16 @@ public void verifyEmailExceptions() throws Exception { String pwHash = "fakehash"; String userEmail = "useremail@asdf.fdas"; - var info = new UserInfo(userId, userEmail, pwHash, System.currentTimeMillis()); - storage.signUp(new TenantIdentifier(null, null, null), info); + storage.signUp(new TenantIdentifier(null, null, null), userId, userEmail, pwHash, System.currentTimeMillis()); try { - storage.signUp(new TenantIdentifier(null, null, null), info); + storage.signUp(new TenantIdentifier(null, null, null), userId, userEmail, pwHash, System.currentTimeMillis()); throw new Exception("This should throw"); } catch (DuplicateUserIdException ex) { // expected } - var info2 = new UserInfo(userId2, userEmail, pwHash, System.currentTimeMillis()); try { - storage.signUp(new TenantIdentifier(null, null, null), info2); + storage.signUp(new TenantIdentifier(null, null, null), userId2, userEmail, pwHash, System.currentTimeMillis()); throw new Exception("This should throw"); } catch (DuplicateEmailException ex) { // expected From 19334544e82099f0456c7bf3661de1fe3b1e922e Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Fri, 28 Apr 2023 11:18:20 +0530 Subject: [PATCH 067/106] fix: test fix (#92) --- .../storage/postgresql/queries/EmailPasswordQueries.java | 2 +- .../supertokens/storage/postgresql/queries/GeneralQueries.java | 2 +- .../storage/postgresql/queries/PasswordlessQueries.java | 2 +- .../storage/postgresql/queries/ThirdPartyQueries.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java index bf065fe2..77451cfa 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java @@ -516,7 +516,7 @@ private static class UserInfoPartial { public final String passwordHash; public UserInfoPartial(String id, String email, String passwordHash, long timeJoined) { - this.id = id; + this.id = id.trim(); this.timeJoined = timeJoined; this.email = email; this.passwordHash = passwordHash; diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java index c40d7b73..75ffe291 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java @@ -966,7 +966,7 @@ public static Map> getTenantIdsForUserIds_transaction(Start }, result -> { Map> finalResult = new HashMap<>(); while (result.next()) { - String userId = result.getString("user_id"); + String userId = result.getString("user_id").trim(); String tenantId = result.getString("tenant_id"); if (!finalResult.containsKey(userId)) { diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java index 4b133772..7023b7eb 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java @@ -902,7 +902,7 @@ private static class UserInfoPartial { public final String phoneNumber; UserInfoPartial(String id, @Nullable String email, @Nullable String phoneNumber, long timeJoined) { - this.id = id; + this.id = id.trim(); this.timeJoined = timeJoined; if (email == null && phoneNumber == null) { diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/ThirdPartyQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/ThirdPartyQueries.java index 10fdbc37..e056d93f 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/ThirdPartyQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/ThirdPartyQueries.java @@ -435,7 +435,7 @@ private static class UserInfoPartial { public final long timeJoined; public UserInfoPartial(String id, String email, UserInfo.ThirdParty thirdParty, long timeJoined) { - this.id = id; + this.id = id.trim(); this.email = email; this.thirdParty = thirdParty; this.timeJoined = timeJoined; From 6c3664e812d9ae67f49d00989bcad17a56e287de Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Fri, 28 Apr 2023 13:54:06 +0530 Subject: [PATCH 068/106] fix: Startup log (#93) * fix: removed log to console * fix: tenant id in loadConfig --- src/main/java/io/supertokens/storage/postgresql/Start.java | 4 ++-- .../io/supertokens/storage/postgresql/config/Config.java | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 54d6f9a3..afd4af13 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -146,8 +146,8 @@ public STORAGE_TYPE getType() { } @Override - public void loadConfig(JsonObject configJson, Set logLevels) throws InvalidConfigException { - Config.loadConfig(this, configJson, logLevels); + public void loadConfig(JsonObject configJson, Set logLevels, TenantIdentifier tenantIdentifier) throws InvalidConfigException { + Config.loadConfig(this, configJson, logLevels, tenantIdentifier); } @Override diff --git a/src/main/java/io/supertokens/storage/postgresql/config/Config.java b/src/main/java/io/supertokens/storage/postgresql/config/Config.java index c41d93ef..e50ec444 100644 --- a/src/main/java/io/supertokens/storage/postgresql/config/Config.java +++ b/src/main/java/io/supertokens/storage/postgresql/config/Config.java @@ -22,6 +22,7 @@ import com.google.gson.JsonObject; import io.supertokens.pluginInterface.LOG_LEVEL; import io.supertokens.pluginInterface.exceptions.InvalidConfigException; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.storage.postgresql.ResourceDistributor; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.output.Logging; @@ -51,13 +52,13 @@ private static Config getInstance(Start start) { return (Config) start.getResourceDistributor().getResource(RESOURCE_KEY); } - public static void loadConfig(Start start, JsonObject configJson, Set logLevels) + public static void loadConfig(Start start, JsonObject configJson, Set logLevels, TenantIdentifier tenantIdentifier) throws InvalidConfigException { if (getInstance(start) != null) { return; } start.getResourceDistributor().setResource(RESOURCE_KEY, new Config(start, configJson, logLevels)); - Logging.info(start, "Loading PostgreSQL config.", true); + Logging.info(start, "Loading PostgreSQL config.", tenantIdentifier.equals(TenantIdentifier.BASE_TENANT)); } public static String getUserPoolId(Start start) { From 2e44c549cef18d35a9665600a8dccd4b2a0b11de Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Mon, 1 May 2023 17:26:08 +0530 Subject: [PATCH 069/106] fix: Userpool test (#94) * fix: userpool test * fix: added test with server restart --- .../TestUserPoolIdChangeBehaviour.java | 164 ++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 src/test/java/io/supertokens/storage/postgresql/test/multitenancy/TestUserPoolIdChangeBehaviour.java diff --git a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/TestUserPoolIdChangeBehaviour.java b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/TestUserPoolIdChangeBehaviour.java new file mode 100644 index 00000000..e8c7493f --- /dev/null +++ b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/TestUserPoolIdChangeBehaviour.java @@ -0,0 +1,164 @@ +/* + * 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.storage.postgresql.test.multitenancy; + +import com.google.gson.JsonObject; +import io.supertokens.ProcessState; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; +import io.supertokens.multitenancy.Multitenancy; +import io.supertokens.multitenancy.exception.BadPermissionException; +import io.supertokens.multitenancy.exception.CannotModifyBaseConfigException; +import io.supertokens.pluginInterface.emailpassword.UserInfo; +import io.supertokens.pluginInterface.exceptions.InvalidConfigException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.*; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.storage.postgresql.test.TestingProcessManager; +import io.supertokens.storage.postgresql.test.Utils; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.thirdparty.InvalidProviderConfigException; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; + +import static org.junit.Assert.*; + +public class TestUserPoolIdChangeBehaviour { + TestingProcessManager.TestingProcess process; + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @After + public void afterEach() throws InterruptedException { + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Before + public void beforeEach() throws InterruptedException, InvalidProviderConfigException, + StorageQueryException, FeatureNotEnabledException, TenantOrAppNotFoundException, IOException, + InvalidConfigException, CannotModifyBaseConfigException, BadPermissionException { + Utils.reset(); + + String[] args = {"../"}; + + this.process = TestingProcessManager.start(args); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY}); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + } + + @Test + public void testUsersWorkAfterUserPoolIdChanges() throws Exception { + JsonObject coreConfig = new JsonObject(); + StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess()) + .modifyConfigToAddANewUserPoolForTesting(coreConfig, 1); + + TenantIdentifier tenantIdentifier = new TenantIdentifier(null, "a1", null); + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + tenantIdentifier, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + coreConfig + ), false); + + TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage( + StorageLayer.getStorage(tenantIdentifier, process.getProcess())); + + String userPoolId = tenantIdentifierWithStorage.getStorage().getUserPoolId(); + + UserInfo userInfo = EmailPassword.signUp( + tenantIdentifierWithStorage, process.getProcess(), "user@example.com", "password"); + + coreConfig.addProperty("postgresql_host", "127.0.0.1"); + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + tenantIdentifier, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + coreConfig + ), false); + + tenantIdentifierWithStorage = tenantIdentifier.withStorage( + StorageLayer.getStorage(tenantIdentifier, process.getProcess())); + String userPoolId2 = tenantIdentifierWithStorage.getStorage().getUserPoolId(); + assertNotEquals(userPoolId, userPoolId2); + + UserInfo user2 = EmailPassword.signIn(tenantIdentifierWithStorage, process.getProcess(), "user@example.com", "password"); + + assertEquals(userInfo, user2); + + } + + @Test + public void testUsersWorkAfterUserPoolIdChangesAndServerRestart() throws Exception { + JsonObject coreConfig = new JsonObject(); + StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess()) + .modifyConfigToAddANewUserPoolForTesting(coreConfig, 1); + + TenantIdentifier tenantIdentifier = new TenantIdentifier(null, "a1", null); + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + tenantIdentifier, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + coreConfig + ), false); + + TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage( + StorageLayer.getStorage(tenantIdentifier, process.getProcess())); + + String userPoolId = tenantIdentifierWithStorage.getStorage().getUserPoolId(); + + UserInfo userInfo = EmailPassword.signUp( + tenantIdentifierWithStorage, process.getProcess(), "user@example.com", "password"); + + coreConfig.addProperty("postgresql_host", "127.0.0.1"); + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + tenantIdentifier, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + coreConfig + ), false); + + // Restart the process + process.kill(false); + String[] args = {"../"}; + this.process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + tenantIdentifierWithStorage = tenantIdentifier.withStorage( + StorageLayer.getStorage(tenantIdentifier, process.getProcess())); + String userPoolId2 = tenantIdentifierWithStorage.getStorage().getUserPoolId(); + assertNotEquals(userPoolId, userPoolId2); + + UserInfo user2 = EmailPassword.signIn(tenantIdentifierWithStorage, process.getProcess(), "user@example.com", "password"); + + assertEquals(userInfo, user2); + } +} From 890192c613d99ec4c4d02a688872a7cc55a7cac6 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Tue, 2 May 2023 15:39:04 +0530 Subject: [PATCH 070/106] fix: delete non auth user (#95) --- .../supertokens/storage/postgresql/Start.java | 30 +++++++++++++++++++ .../queries/EmailVerificationQueries.java | 13 ++++++++ .../postgresql/queries/SessionQueries.java | 13 ++++++++ .../postgresql/queries/TOTPQueries.java | 13 ++++++++ 4 files changed, 69 insertions(+) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index afd4af13..3007b4bd 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -489,6 +489,16 @@ public void deleteSessionsOfUser(AppIdentifier appIdentifier, String userId) } } + @Override + public boolean deleteSessionsOfUser(TenantIdentifier tenantIdentifier, String userId) + throws StorageQueryException { + try { + return SessionQueries.deleteSessionsOfUser(this, tenantIdentifier, userId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + @Override public int getNumberOfSessions(TenantIdentifier tenantIdentifier) throws StorageQueryException { try { @@ -1031,6 +1041,16 @@ public void deleteEmailVerificationUserInfo(AppIdentifier appIdentifier, String } } + @Override + public boolean deleteEmailVerificationUserInfo(TenantIdentifier tenantIdentifier, String userId) + throws StorageQueryException { + try { + return EmailVerificationQueries.deleteUserInfo(this, tenantIdentifier, userId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + @Override public void addEmailVerificationToken(TenantIdentifier tenantIdentifier, EmailVerificationTokenInfo emailVerificationInfo) @@ -2609,6 +2629,16 @@ public void removeUser_Transaction(TransactionConnection con, AppIdentifier appI } } + @Override + public boolean removeUser(TenantIdentifier tenantIdentifier, String userId) + throws StorageQueryException { + try { + return TOTPQueries.removeUser(this, tenantIdentifier, userId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + @Override public void updateDeviceName(AppIdentifier appIdentifier, String userId, String oldDeviceName, String newDeviceName) throws StorageQueryException, DeviceAlreadyExistsException, diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/EmailVerificationQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/EmailVerificationQueries.java index 472a5184..b24bb04e 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/EmailVerificationQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/EmailVerificationQueries.java @@ -255,6 +255,19 @@ public static void deleteUserInfo(Start start, AppIdentifier appIdentifier, Stri }); } + public static boolean deleteUserInfo(Start start, TenantIdentifier tenantIdentifier, String userId) + throws StorageQueryException, SQLException { + String QUERY = "DELETE FROM " + getConfig(start).getEmailVerificationTokensTable() + + " WHERE app_id = ? AND tenant_id = ? AND user_id = ?"; + + int numRows = update(start, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, userId); + }); + return numRows > 0; + } + public static void unverifyEmail(Start start, AppIdentifier appIdentifier, String userId, String email) throws SQLException, StorageQueryException { String QUERY = "DELETE FROM " + getConfig(start).getEmailVerificationTable() diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/SessionQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/SessionQueries.java index 40210f55..a311f7a3 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/SessionQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/SessionQueries.java @@ -199,6 +199,19 @@ public static void deleteSessionsOfUser(Start start, AppIdentifier appIdentifier }); } + public static boolean deleteSessionsOfUser(Start start, TenantIdentifier tenantIdentifier, String userId) + throws SQLException, StorageQueryException { + String QUERY = "DELETE FROM " + getConfig(start).getSessionInfoTable() + + " WHERE app_id = ? AND tenant_id = ? AND user_id = ?"; + + int numRows = update(start, QUERY.toString(), pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, userId); + }); + return numRows > 0; + } + public static String[] getAllNonExpiredSessionHandlesForUser(Start start, TenantIdentifier tenantIdentifier, String userId) throws SQLException, StorageQueryException { diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/TOTPQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/TOTPQueries.java index 2b7805ba..50234cd5 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/TOTPQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/TOTPQueries.java @@ -167,6 +167,19 @@ public static int removeUser_Transaction(Start start, Connection con, AppIdentif return removedUsersCount; } + public static boolean removeUser(Start start, TenantIdentifier tenantIdentifier, String userId) + throws SQLException, StorageQueryException { + String QUERY = "DELETE FROM " + Config.getConfig(start).getTotpUsedCodesTable() + + " WHERE app_id = ? AND tenant_id = ? AND user_id = ?;"; + int removedUsersCount = update(start, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, userId); + }); + + return removedUsersCount > 0; + } + public static int updateDeviceName(Start start, AppIdentifier appIdentifier, String userId, String oldDeviceName, String newDeviceName) throws StorageQueryException, SQLException { String QUERY = "UPDATE " + Config.getConfig(start).getTotpUserDevicesTable() From 71485b716f0d9dfb41053e90be016813deaef609 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Thu, 4 May 2023 12:51:09 +0530 Subject: [PATCH 071/106] fix: Delete nonauth user (#96) * fix: nonAuthRecipeuserData to take tenantIdentifier * fix: pr comments --- .../supertokens/storage/postgresql/Start.java | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 3007b4bd..2682a38f 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -714,11 +714,11 @@ public boolean isUserIdBeingUsedInNonAuthRecipe(AppIdentifier appIdentifier, Str @TestOnly @Override - public void addInfoToNonAuthRecipesBasedOnUserId(String className, String userId) throws StorageQueryException { + public void addInfoToNonAuthRecipesBasedOnUserId(TenantIdentifier tenantIdentifier, String className, String userId) throws StorageQueryException { // add entries to nonAuthRecipe tables with input userId if (className.equals(SessionStorage.class.getName())) { try { - createNewSession(new TenantIdentifier(null, null, null), "sessionHandle", userId, "refreshTokenHash", + createNewSession(tenantIdentifier, "sessionHandle", userId, "refreshTokenHash", new JsonObject(), System.currentTimeMillis() + 1000000, new JsonObject(), System.currentTimeMillis(), false); } catch (Exception e) { @@ -729,14 +729,14 @@ public void addInfoToNonAuthRecipesBasedOnUserId(String className, String userId String role = "testRole"; this.startTransaction(con -> { try { - createNewRoleOrDoNothingIfExists_Transaction(new AppIdentifier(null, null), con, role); + createNewRoleOrDoNothingIfExists_Transaction(tenantIdentifier.toAppIdentifier(), con, role); } catch (TenantOrAppNotFoundException e) { throw new IllegalStateException(e); } return null; }); try { - addRoleToUser(new TenantIdentifier(null, null, null), userId, role); + addRoleToUser(tenantIdentifier, userId, role); } catch (Exception e) { throw new StorageTransactionLogicException(e); } @@ -747,7 +747,7 @@ public void addInfoToNonAuthRecipesBasedOnUserId(String className, String userId try { EmailVerificationTokenInfo info = new EmailVerificationTokenInfo(userId, "someToken", 10000, "test123@example.com"); - addEmailVerificationToken(new TenantIdentifier(null, null, null), info); + addEmailVerificationToken(tenantIdentifier, info); } catch (DuplicateEmailVerificationTokenException e) { throw new StorageQueryException(e); @@ -760,7 +760,7 @@ public void addInfoToNonAuthRecipesBasedOnUserId(String className, String userId try { this.startTransaction(con -> { try { - setUserMetadata_Transaction(new AppIdentifier(null, null), con, userId, data); + setUserMetadata_Transaction(tenantIdentifier.toAppIdentifier(), con, userId, data); } catch (TenantOrAppNotFoundException e) { throw new StorageTransactionLogicException(e); } @@ -775,7 +775,18 @@ public void addInfoToNonAuthRecipesBasedOnUserId(String className, String userId } else if (className.equals(TOTPStorage.class.getName())) { try { TOTPDevice device = new TOTPDevice(userId, "testDevice", "secret", 0, 30, false); - TOTPQueries.createDevice(this, new AppIdentifier(null, null), device); + TOTPQueries.createDevice(this, tenantIdentifier.toAppIdentifier(), device); + this.startTransaction(con -> { + try { + long now = System.currentTimeMillis(); + TOTPQueries.insertUsedCode_Transaction(this, + (Connection) con.getConnection(), tenantIdentifier, new TOTPUsedCode(userId, "123456", true, 1000+now, now)); + } catch (SQLException e) { + throw new StorageTransactionLogicException(e); + } + return null; + }); + } catch (StorageTransactionLogicException e) { throw new StorageQueryException(e.actualException); } @@ -2347,7 +2358,7 @@ public boolean addUserIdToTenant(TenantIdentifier tenantIdentifier, String userI @Override public boolean removeUserIdFromTenant(TenantIdentifier tenantIdentifier, String userId) - throws StorageQueryException, UnknownUserIdException { + throws StorageQueryException { try { return this.startTransaction(con -> { Connection sqlCon = (Connection) con.getConnection(); @@ -2356,7 +2367,8 @@ public boolean removeUserIdFromTenant(TenantIdentifier tenantIdentifier, String userId); if (recipeId == null) { - throw new StorageTransactionLogicException(new UnknownUserIdException()); + sqlCon.commit(); + return false; // No auth user to remove } boolean removed; @@ -2382,8 +2394,6 @@ public boolean removeUserIdFromTenant(TenantIdentifier tenantIdentifier, String ServerErrorMessage serverErrorMessage = ((PSQLException) e.actualException).getServerErrorMessage(); throw new StorageQueryException(e.actualException); - } else if (e.actualException instanceof UnknownUserIdException) { - throw (UnknownUserIdException) e.actualException; } else if (e.actualException instanceof StorageQueryException) { throw (StorageQueryException) e.actualException; } From 1797df209d69c279cc6f371ba3f9736178e7859d Mon Sep 17 00:00:00 2001 From: KShivendu Date: Thu, 4 May 2023 16:55:02 +0530 Subject: [PATCH 072/106] feat: Add active user stat queries for MFA --- .../supertokens/storage/postgresql/Start.java | 34 +++++++++++++++++++ .../queries/ActiveUsersQueries.java | 26 ++++++++++++++ .../postgresql/queries/MfaQueries.java | 12 +++++++ 3 files changed, 72 insertions(+) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 79d16c87..5cd95c3c 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -1311,6 +1311,27 @@ public int countUsersEnabledTotpAndActiveSince(AppIdentifier appIdentifier, long } } + @Override + public int countUsersEnabledMfa(AppIdentifier appIdentifier) throws StorageQueryException { + try { + // TODO... + return ActiveUsersQueries.countUsersEnabledMfa(this); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public int countUsersEnabledMfaAndActiveSince(AppIdentifier appIdentifier, long time) + throws StorageQueryException { + try { + // TODO... + return ActiveUsersQueries.countUsersEnabledMfaAndActiveSince(this, time); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + @Override public boolean doesUserIdExist(TenantIdentifier tenantIdentifierIdentifier, String userId) throws StorageQueryException { @@ -2655,4 +2676,17 @@ public boolean disableFactor(TenantIdentifier tenantIdentifier, String userId, S } } + @Override + public boolean deleteUser(AppIdentifier appIdentifier, String userId) throws StorageQueryException { + try { + int deletedCount = MfaQueries.deleteUser(this, appIdentifier, userId); + if (deletedCount == 0) { + return false; + } + return true; + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java index 1be94685..0951e737 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java @@ -54,6 +54,32 @@ public static int countUsersEnabledTotpAndActiveSince(Start start, long sinceTim }); } + public static int countUsersEnabledMfa(Start start) throws SQLException, StorageQueryException { + String QUERY = "SELECT COUNT(*) as total FROM (SELECT DISTINCT user_id FROM " + Config.getConfig(start).getMfaUserFactorsTable() + ") AS mfa_users"; + + return execute(start, QUERY, null, result -> { + if (result.next()) { + return result.getInt("total"); + } + return 0; + }); + } + + public static int countUsersEnabledMfaAndActiveSince(Start start, long sinceTime) throws SQLException, StorageQueryException { + // Find unique users from mfa_user_factors table and join with user_last_active table + String QUERY = "SELECT COUNT(*) as total FROM (SELECT DISTINCT user_id FROM " + Config.getConfig(start).getMfaUserFactorsTable() + ") AS mfa_users " + + "INNER JOIN " + Config.getConfig(start).getUserLastActiveTable() + " AS user_last_active " + + "ON mfa_users.user_id = user_last_active.user_id " + + "WHERE user_last_active.last_active_time >= ?"; + + return execute(start, QUERY, pst -> pst.setLong(1, sinceTime), result -> { + if (result.next()) { + return result.getInt("total"); + } + return 0; + }); + } + public static int updateUserLastActive(Start start, String userId) throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + Config.getConfig(start).getUserLastActiveTable() + "(user_id, last_active_time) VALUES(?, ?) ON CONFLICT(user_id) DO UPDATE SET last_active_time = ?"; diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java index bbf61e85..26005b13 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java @@ -16,6 +16,7 @@ package io.supertokens.storage.postgresql.queries; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.config.Config; @@ -83,4 +84,15 @@ public static int disableFactor(Start start, TenantIdentifier tenantIdentifier, }); } + + public static int deleteUser(Start start, AppIdentifier appIdentifier, String userId) + throws StorageQueryException, SQLException { + String QUERY = "DELETE FROM " + Config.getConfig(start).getMfaUserFactorsTable() + " WHERE app_id = ? AND user_id = ?"; + + return update(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); + } + } From 77beacc6fe26aa265ac91cca95607de58db94610 Mon Sep 17 00:00:00 2001 From: KShivendu Date: Thu, 4 May 2023 17:00:04 +0530 Subject: [PATCH 073/106] fix: Update user_id length in mfa_user_factors table --- .../io/supertokens/storage/postgresql/queries/MfaQueries.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java index 26005b13..f49c493e 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java @@ -34,7 +34,7 @@ public static String getQueryToCreateUserFactorsTable(Start start) { return "CREATE TABLE IF NOT EXISTS " + Config.getConfig(start).getMfaUserFactorsTable() + " (" + "app_id VARCHAR(64) DEFAULT 'public'," + "tenant_id VARCHAR(64) DEFAULT 'public'," - + "user_id VARCHAR(255) NOT NULL," + + "user_id VARCHAR(128) NOT NULL," + "factor_id VARCHAR(255) NOT NULL," + "PRIMARY KEY (app_id, tenant_id, user_id, factor_id)," + "FOREIGN KEY (app_id, tenant_id)" From 0744c44df2b4cec431124cee97d399dc4ec33181 Mon Sep 17 00:00:00 2001 From: KShivendu Date: Thu, 4 May 2023 17:05:44 +0530 Subject: [PATCH 074/106] Set factor_id VARCHAR length to 16 --- .../io/supertokens/storage/postgresql/queries/MfaQueries.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java index f49c493e..a43f1bb4 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java @@ -35,7 +35,7 @@ public static String getQueryToCreateUserFactorsTable(Start start) { + "app_id VARCHAR(64) DEFAULT 'public'," + "tenant_id VARCHAR(64) DEFAULT 'public'," + "user_id VARCHAR(128) NOT NULL," - + "factor_id VARCHAR(255) NOT NULL," + + "factor_id VARCHAR(16) NOT NULL," + "PRIMARY KEY (app_id, tenant_id, user_id, factor_id)," + "FOREIGN KEY (app_id, tenant_id)" + "REFERENCES " + Config.getConfig(start).getTenantsTable() + " (app_id, tenant_id) ON DELETE CASCADE"; From 2d00098b17e5dc4ab3bc2f13d49f1b243e4e9ad3 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Thu, 4 May 2023 18:47:59 +0530 Subject: [PATCH 075/106] fix: config validation (#97) --- .../supertokens/storage/postgresql/Start.java | 10 ++++++---- .../postgresql/config/PostgreSQLConfig.java | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 2682a38f..95e1e6cf 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -98,10 +98,7 @@ import java.sql.Connection; import java.sql.SQLException; import java.sql.SQLTransactionRollbackException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Set; +import java.util.*; public class Start implements SessionSQLStorage, EmailPasswordSQLStorage, EmailVerificationSQLStorage, ThirdPartySQLStorage, @@ -2734,4 +2731,9 @@ public int removeExpiredCodes(TenantIdentifier tenantIdentifier, long expiredBef throw new StorageQueryException(e); } } + + @Override + public Set getValidFieldsInConfig() { + return PostgreSQLConfig.getValidFields(); + } } diff --git a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java index de7020e0..7d043d98 100644 --- a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java +++ b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java @@ -19,9 +19,15 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import io.supertokens.pluginInterface.exceptions.InvalidConfigException; import java.net.URI; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; @JsonIgnoreProperties(ignoreUnknown = true) public class PostgreSQLConfig { @@ -77,6 +83,17 @@ public class PostgreSQLConfig { @JsonProperty private String postgresql_connection_uri = null; + public static Set getValidFields() { + PostgreSQLConfig config = new PostgreSQLConfig(); + JsonObject configObj = new GsonBuilder().serializeNulls().create().toJsonTree(config).getAsJsonObject(); + + Set validFields = new HashSet<>(); + for (Map.Entry entry : configObj.entrySet()) { + validFields.add(entry.getKey()); + } + return validFields; + } + public String getTableSchema() { if (postgresql_connection_uri != null) { String connectionAttributes = getConnectionAttributes(); From 8a7c69661c2ab89c839489752a6f874cb34d4fae Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Fri, 5 May 2023 15:47:05 +0530 Subject: [PATCH 076/106] fix: config per tenant, per app annotations (#98) --- config.yaml | 53 +++++++++++++++++++++++++----------------------- devConfig.yaml | 55 +++++++++++++++++++++++++++----------------------- 2 files changed, 58 insertions(+), 50 deletions(-) diff --git a/config.yaml b/config.yaml index 63518243..36459b8d 100644 --- a/config.yaml +++ b/config.yaml @@ -1,66 +1,69 @@ postgresql_config_version: 0 -# (OPTIONAL | Default: 10) integer value. Defines the connection pool size to PostgreSQL. +# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: 10) integer value. Defines the connection pool size to PostgreSQL. # Please see https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing # postgresql_connection_pool_size: -# (OPTIONAL | Default: null) string value. Specify the PostgreSQL connection URI in the following -# format: postgresql://[user[:[password]]@]host[:port][/dbname][?attr1=val1&attr2=val2... +# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: null) string value. Specify the PostgreSQL connection URI in the +# following format: postgresql://[user[:[password]]@]host[:port][/dbname][?attr1=val1&attr2=val2... # Values provided via other configs will override values provided by this config. # postgresql_connection_uri: -# (OPTIONAL | Default: "localhost") string value. Specify the postgresql host url here. For example: +# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "localhost") string value. Specify the postgresql host url here. +# For example: # - "localhost" # - "192.168.0.1" # - "" # - "example.com" # postgresql_host: -# (OPTIONAL | Default: 5432) integer value. Specify the port to use when connecting to PostgreSQL instance. +# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: 5432) integer value. Specify the port to use when connecting to +# PostgreSQL instance. # postgresql_port: -# (COMPULSORY) string value. The PostgreSQL user to use to query the database. +# (DIFFERENT_ACROSS_TENANTS | COMPULSORY) string value. The PostgreSQL user to use to query the database. # If the relevant tables are not already created by you, this user should have the # ability to create new tables. To see the tables needed, visit: https://supertokens.io/docs/community/getting-started/database-setup/postgresql # postgresql_user: -# (COMPULSORY) string value. Password for the PostgreSQL user. If you have not set a password +# (DIFFERENT_ACROSS_TENANTS | COMPULSORY) string value. Password for the PostgreSQL user. If you have not set a password # make this an empty string. # postgresql_password: -# (OPTIONAL | Default: "supertokens") string value. The database name to store SuperTokens related data. +# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "supertokens") string value. The database name to store SuperTokens +# related data. # postgresql_database_name: -# (OPTIONAL | Default: "public") string value. The schema for tables. +# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "public") string value. The schema for tables. # postgresql_table_schema: -# (OPTIONAL | Default: "") string value. A prefix to add to all table names managed by SuperTokens. An "_" will be -# added between this prefix and the actual table name if the prefix is defined +# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "") string value. A prefix to add to all table names managed by +# SuperTokens. An "_" will be added between this prefix and the actual table name if the prefix is defined # postgresql_table_names_prefix: -# (OPTIONAL | Default: "key_value") string value. Specify the name of the table that will store secret keys -# and app info necessary for the functioning sessions. +# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "key_value") string value. Specify the name of the table that will +# store secret keys and app info necessary for the functioning sessions. # postgresql_key_value_table_name: -# (OPTIONAL | Default: "session_info") string value. Specify the name of the table that will store the -# session info for users. +# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "session_info") string value. Specify the name of the table that +# will store the session info for users. # postgresql_session_info_table_name: -# (OPTIONAL | Default: "emailpassword_users") string value. Specify the name of the table that will store the -# user information, along with their email and hashed password. +# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "emailpassword_users") string value. Specify the name of the table +# that will store the user information, along with their email and hashed password. # postgresql_emailpassword_users_table_name: -# (OPTIONAL | Default: "emailpassword_pswd_reset_tokens") string value. Specify the name of the table that will -# store the password reset tokens for users. +# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "emailpassword_pswd_reset_tokens") string value. Specify the name +# of the table that will store the password reset tokens for users. # postgresql_emailpassword_pswd_reset_tokens_table_name: -# (OPTIONAL | Default: "emailverification_tokens") string value. Specify the name of the table that will -# store the email verification tokens for users. +# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "emailverification_tokens") string value. Specify the name of the +# table that will store the email verification tokens for users. # postgresql_emailverification_tokens_table_name: -# (OPTIONAL | Default: "emailverification_verified_emails") string value. Specify the name of the table that will -# store the verified email addresses. +# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "emailverification_verified_emails") string value. Specify the name +# of the table that will store the verified email addresses. # postgresql_emailverification_verified_emails_table_name: -# (OPTIONAL | Default: "thirdparty_users") string value. Specify the name of the table that will -# store the thirdparty recipe users. +# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "thirdparty_users") string value. Specify the name of the table +# that will store the thirdparty recipe users. # postgresql_thirdparty_users_table_name diff --git a/devConfig.yaml b/devConfig.yaml index e6a665b7..39d0d5ed 100644 --- a/devConfig.yaml +++ b/devConfig.yaml @@ -1,66 +1,71 @@ postgresql_config_version: 0 -# (OPTIONAL | Default: 10) integer value. Defines the connection pool size to PostgreSQL. +# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: 10) integer value. Defines the connection pool size to PostgreSQL. # Please see https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing # postgresql_connection_pool_size: -# (OPTIONAL | Default: null) string value. Specify the PostgreSQL connection URI in the following -# format: postgresql://[user[:[password]]@]host[:port][/dbname][?attr1=val1&attr2=val2... +# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: null) string value. Specify the PostgreSQL connection URI in the +# following format: postgresql://[user[:[password]]@]host[:port][/dbname][?attr1=val1&attr2=val2... # Values provided via other configs will override values provided by this config. # postgresql_connection_uri: -# (OPTIONAL | Default: "localhost") string value. Specify the postgresql host url here. For example: +# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "localhost") string value. Specify the postgresql host url here. +# For example: # - "localhost" # - "192.168.0.1" # - "" # - "example.com" # postgresql_host: -# (OPTIONAL | Default: 5432) integer value. Specify the port to use when connecting to PostgreSQL instance. +# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: 5432) integer value. Specify the port to use when connecting to +# PostgreSQL instance. # postgresql_port: -# (COMPULSORY) string value. The PostgreSQL user to use to query the database. +# (DIFFERENT_ACROSS_TENANTS | COMPULSORY) string value. The PostgreSQL user to use to query the database. # If the relevant tables are not already created by you, this user should have the # ability to create new tables. To see the tables needed, visit: TODO postgresql_user: "root" -# (COMPULSORY) string value. Password for the PostgreSQL instance. If you do not have a password +# (DIFFERENT_ACROSS_TENANTS | COMPULSORY) string value. Password for the PostgreSQL instance. If you do not have a +# password # make this an empty string. postgresql_password: "root" -# (OPTIONAL | Default: "supertokens") string value. The database name to store SuperTokens related data. + +# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "supertokens") string value. The database name to store SuperTokens +# related data. # postgresql_database_name: -# (OPTIONAL | Default: "public") string value. The schema for tables. +# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "public") string value. The schema for tables. # postgresql_table_schema: -# (OPTIONAL | Default: "") string value. A prefix to add to all table names managed by SuperTokens. An "_" will be -# added between this prefix and the actual table name if the prefix is defined +# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "") string value. A prefix to add to all table names managed by +# SuperTokens. An "_" will be added between this prefix and the actual table name if the prefix is defined # postgresql_table_names_prefix: -# (OPTIONAL | Default: "key_value") string value. Specify the name of the table that will store secret keys -# and app info necessary for the functioning sessions. +# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "key_value") string value. Specify the name of the table that will +# store secret keys and app info necessary for the functioning sessions. # postgresql_key_value_table_name: -# (OPTIONAL | Default: "session_info") string value. Specify the name of the table that will store the -# session info for users. +# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "session_info") string value. Specify the name of the table that +# will store the session info for users. # postgresql_session_info_table_name: -# (OPTIONAL | Default: "emailpassword_users") string value. Specify the name of the table that will store the -# user information, along with their email and hashed password. +# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "emailpassword_users") string value. Specify the name of the table +# that will store the user information, along with their email and hashed password. # postgresql_emailpassword_users_table_name: -# (OPTIONAL | Default: "emailpassword_pswd_reset_tokens") string value. Specify the name of the table that will -# store the password reset tokens for users. +# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "emailpassword_pswd_reset_tokens") string value. Specify the name +# of the table that will store the password reset tokens for users. # postgresql_emailpassword_pswd_reset_tokens_table_name: -# (OPTIONAL | Default: "emailverification_tokens") string value. Specify the name of the table that will -# store the email verification tokens for users. +# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "emailverification_tokens") string value. Specify the name of the +# table that will store the email verification tokens for users. # postgresql_emailverification_tokens_table_name: -# (OPTIONAL | Default: "emailverification_verified_emails") string value. Specify the name of the table that will -# store the verified email addresses. +# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "emailverification_verified_emails") string value. Specify the name +# of the table that will store the verified email addresses. # postgresql_emailverification_verified_emails_table_name: -# (OPTIONAL | Default: "thirdparty_users") string value. Specify the name of the table that will -# store the thirdparty recipe users. +# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "thirdparty_users") string value. Specify the name of the table +# that will store the thirdparty recipe users. # postgresql_thirdparty_users_table_name From 4e1d22b93b43782147bbac72cac8abada0505f57 Mon Sep 17 00:00:00 2001 From: KShivendu Date: Wed, 10 May 2023 13:42:10 +0530 Subject: [PATCH 077/106] feat: Consider multitenancy when getting MFA stats --- .../supertokens/storage/postgresql/Start.java | 6 ++---- .../postgresql/queries/ActiveUsersQueries.java | 18 ++++++++++++------ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index ea2289ec..e42d79c2 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -1339,8 +1339,7 @@ public int countUsersEnabledTotpAndActiveSince(AppIdentifier appIdentifier, long @Override public int countUsersEnabledMfa(AppIdentifier appIdentifier) throws StorageQueryException { try { - // TODO... - return ActiveUsersQueries.countUsersEnabledMfa(this); + return ActiveUsersQueries.countUsersEnabledMfa(this, appIdentifier); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1350,8 +1349,7 @@ public int countUsersEnabledMfa(AppIdentifier appIdentifier) throws StorageQuery public int countUsersEnabledMfaAndActiveSince(AppIdentifier appIdentifier, long time) throws StorageQueryException { try { - // TODO... - return ActiveUsersQueries.countUsersEnabledMfaAndActiveSince(this, time); + return ActiveUsersQueries.countUsersEnabledMfaAndActiveSince(this, appIdentifier, time); } catch (SQLException e) { throw new StorageQueryException(e); } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java index ec1c617b..62ad6752 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java @@ -65,10 +65,12 @@ public static int countUsersEnabledTotpAndActiveSince(Start start, AppIdentifier }); } - public static int countUsersEnabledMfa(Start start) throws SQLException, StorageQueryException { - String QUERY = "SELECT COUNT(*) as total FROM (SELECT DISTINCT user_id FROM " + Config.getConfig(start).getMfaUserFactorsTable() + ") AS mfa_users"; + public static int countUsersEnabledMfa(Start start, AppIdentifier appIdentifier) throws SQLException, StorageQueryException { + String QUERY = "SELECT COUNT(*) as total FROM (SELECT DISTINCT user_id FROM " + Config.getConfig(start).getMfaUserFactorsTable() + "WHERE app_id = ?) AS app_mfa_users"; - return execute(start, QUERY, null, result -> { + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + }, result -> { if (result.next()) { return result.getInt("total"); } @@ -76,14 +78,18 @@ public static int countUsersEnabledMfa(Start start) throws SQLException, Storage }); } - public static int countUsersEnabledMfaAndActiveSince(Start start, long sinceTime) throws SQLException, StorageQueryException { + public static int countUsersEnabledMfaAndActiveSince(Start start, AppIdentifier appIdentifier, long sinceTime) throws SQLException, StorageQueryException { // Find unique users from mfa_user_factors table and join with user_last_active table String QUERY = "SELECT COUNT(*) as total FROM (SELECT DISTINCT user_id FROM " + Config.getConfig(start).getMfaUserFactorsTable() + ") AS mfa_users " + "INNER JOIN " + Config.getConfig(start).getUserLastActiveTable() + " AS user_last_active " + "ON mfa_users.user_id = user_last_active.user_id " - + "WHERE user_last_active.last_active_time >= ?"; + + "WHERE user_last_active.app_id = ?" + + "AND user_last_active.last_active_time >= ?"; - return execute(start, QUERY, pst -> pst.setLong(1, sinceTime), result -> { + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setLong(2, sinceTime); + }, result -> { if (result.next()) { return result.getInt("total"); } From 2268cf2319aca2d1f4dc29f76bc3627afaadc56d Mon Sep 17 00:00:00 2001 From: KShivendu Date: Wed, 10 May 2023 16:24:34 +0530 Subject: [PATCH 078/106] test: Fix mistake in MFA table create query --- .../io/supertokens/storage/postgresql/queries/MfaQueries.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java index a43f1bb4..7249b4bf 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java @@ -38,7 +38,7 @@ public static String getQueryToCreateUserFactorsTable(Start start) { + "factor_id VARCHAR(16) NOT NULL," + "PRIMARY KEY (app_id, tenant_id, user_id, factor_id)," + "FOREIGN KEY (app_id, tenant_id)" - + "REFERENCES " + Config.getConfig(start).getTenantsTable() + " (app_id, tenant_id) ON DELETE CASCADE"; + + "REFERENCES " + Config.getConfig(start).getTenantsTable() + " (app_id, tenant_id) ON DELETE CASCADE);"; } public static int enableFactor(Start start, TenantIdentifier tenantIdentifier, String userId, String factorId) From 2b30ca9163afbd8b9307cc8f74dc0084c3bbf20e Mon Sep 17 00:00:00 2001 From: KShivendu Date: Wed, 10 May 2023 18:33:51 +0530 Subject: [PATCH 079/106] feat: Add query to delete user from a tenant --- .../io/supertokens/storage/postgresql/Start.java | 13 +++++++++++++ .../storage/postgresql/queries/MfaQueries.java | 11 +++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index e42d79c2..5b7b20be 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -2804,6 +2804,19 @@ public boolean deleteUser(AppIdentifier appIdentifier, String userId) throws Sto } } + @Override + public boolean deleteUserFromTenant(TenantIdentifier tenantIdentifier, String userId) throws StorageQueryException { + try { + int deletedCount = MfaQueries.deleteUserFromTenant(this, tenantIdentifier, userId); + if (deletedCount == 0) { + return false; + } + return true; + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + @Override public Set getValidFieldsInConfig() { diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java index 7249b4bf..1e856fa1 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java @@ -95,4 +95,15 @@ public static int deleteUser(Start start, AppIdentifier appIdentifier, String us }); } + public static int deleteUserFromTenant(Start start, TenantIdentifier tenantIdentifier, String userId) + throws StorageQueryException, SQLException { + String QUERY = "DELETE FROM " + Config.getConfig(start).getMfaUserFactorsTable() + " WHERE app_id = ? AND tenant_id = ? AND user_id = ?"; + + return update(start, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, userId); + }); + } + } From bfda93fd58e0569a93b6ae29d71db6aa49f7a7aa Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Fri, 12 May 2023 15:56:35 +0530 Subject: [PATCH 080/106] fix: config annotation (#102) * fix: config annotation * fix: removed comments * Update src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java * Update src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java --------- Co-authored-by: Rishabh Poddar --- .../supertokens/storage/postgresql/Start.java | 7 + .../annotations/ConnectionPoolProperty.java | 27 ++ .../annotations/IgnoreForAnnotationCheck.java | 27 ++ .../NotConflictingWithinUserPool.java | 27 ++ .../annotations/UserPoolProperty.java | 27 ++ .../storage/postgresql/config/Config.java | 11 +- .../postgresql/config/PostgreSQLConfig.java | 284 ++++++++++-------- .../storage/postgresql/test/ConfigTest.java | 22 +- .../test/multitenancy/StorageLayerTest.java | 6 +- 9 files changed, 301 insertions(+), 137 deletions(-) create mode 100644 src/main/java/io/supertokens/storage/postgresql/annotations/ConnectionPoolProperty.java create mode 100644 src/main/java/io/supertokens/storage/postgresql/annotations/IgnoreForAnnotationCheck.java create mode 100644 src/main/java/io/supertokens/storage/postgresql/annotations/NotConflictingWithinUserPool.java create mode 100644 src/main/java/io/supertokens/storage/postgresql/annotations/UserPoolProperty.java diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 95e1e6cf..6d30988a 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -1385,17 +1385,24 @@ public void setJWTSigningKey_Transaction(AppIdentifier appIdentifier, Transactio private boolean isUniqueConstraintError(ServerErrorMessage serverMessage, String tableName, String columnName) { + String[] tableNameParts = tableName.split("\\."); + tableName = tableNameParts[tableNameParts.length - 1]; + return serverMessage.getSQLState().equals("23505") && serverMessage.getConstraint() != null && serverMessage.getConstraint().equals(tableName + "_" + columnName + "_key"); } private boolean isForeignKeyConstraintError(ServerErrorMessage serverMessage, String tableName, String columnName) { + String[] tableNameParts = tableName.split("\\."); + tableName = tableNameParts[tableNameParts.length - 1]; return serverMessage.getSQLState().equals("23503") && serverMessage.getConstraint() != null && serverMessage.getConstraint().equals(tableName + "_" + columnName + "_fkey"); } private boolean isPrimaryKeyError(ServerErrorMessage serverMessage, String tableName) { + String[] tableNameParts = tableName.split("\\."); + tableName = tableNameParts[tableNameParts.length - 1]; return serverMessage.getSQLState().equals("23505") && serverMessage.getConstraint() != null && serverMessage.getConstraint().equals(tableName + "_pkey"); } diff --git a/src/main/java/io/supertokens/storage/postgresql/annotations/ConnectionPoolProperty.java b/src/main/java/io/supertokens/storage/postgresql/annotations/ConnectionPoolProperty.java new file mode 100644 index 00000000..4dd4ee6f --- /dev/null +++ b/src/main/java/io/supertokens/storage/postgresql/annotations/ConnectionPoolProperty.java @@ -0,0 +1,27 @@ +/* + * 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.storage.postgresql.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface ConnectionPoolProperty { +} diff --git a/src/main/java/io/supertokens/storage/postgresql/annotations/IgnoreForAnnotationCheck.java b/src/main/java/io/supertokens/storage/postgresql/annotations/IgnoreForAnnotationCheck.java new file mode 100644 index 00000000..e5770c7c --- /dev/null +++ b/src/main/java/io/supertokens/storage/postgresql/annotations/IgnoreForAnnotationCheck.java @@ -0,0 +1,27 @@ +/* + * 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.storage.postgresql.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface IgnoreForAnnotationCheck { +} diff --git a/src/main/java/io/supertokens/storage/postgresql/annotations/NotConflictingWithinUserPool.java b/src/main/java/io/supertokens/storage/postgresql/annotations/NotConflictingWithinUserPool.java new file mode 100644 index 00000000..2fd6fafc --- /dev/null +++ b/src/main/java/io/supertokens/storage/postgresql/annotations/NotConflictingWithinUserPool.java @@ -0,0 +1,27 @@ +/* + * 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.storage.postgresql.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface NotConflictingWithinUserPool { +} diff --git a/src/main/java/io/supertokens/storage/postgresql/annotations/UserPoolProperty.java b/src/main/java/io/supertokens/storage/postgresql/annotations/UserPoolProperty.java new file mode 100644 index 00000000..19b33114 --- /dev/null +++ b/src/main/java/io/supertokens/storage/postgresql/annotations/UserPoolProperty.java @@ -0,0 +1,27 @@ +/* + * 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.storage.postgresql.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface UserPoolProperty { +} diff --git a/src/main/java/io/supertokens/storage/postgresql/config/Config.java b/src/main/java/io/supertokens/storage/postgresql/config/Config.java index e50ec444..e6da737d 100644 --- a/src/main/java/io/supertokens/storage/postgresql/config/Config.java +++ b/src/main/java/io/supertokens/storage/postgresql/config/Config.java @@ -65,16 +65,11 @@ public static String getUserPoolId(Start start) { // TODO: The way things are implemented right now, this function has the issue that if the user points to the // same database, but with a different host (cause the db is reachable via two hosts as an example), // then it will return two different user pool IDs - which is technically the wrong thing to do. - PostgreSQLConfig config = getConfig(start); - return config.getDatabaseName() + "|" + config.getHostName() + "|" + config.getTableSchema() + "|" + - config.getPort(); + return getConfig(start).getUserPoolId(); } public static String getConnectionPoolId(Start start) { - PostgreSQLConfig config = getConfig(start); - return config.getConnectionScheme() + "|" + config.getConnectionAttributes() + "|" + config.getUser() + "|" + - config.getPassword() + "|" + config.getConnectionPoolSize(); - + return getConfig(start).getConnectionPoolId(); } public static void assertThatConfigFromSameUserPoolIsNotConflicting(Start start, JsonObject otherConfigJson) @@ -100,7 +95,7 @@ public static Set getLogLevels(Start start) { private PostgreSQLConfig loadPostgreSQLConfig(JsonObject configJson) throws IOException, InvalidConfigException { final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); PostgreSQLConfig config = mapper.readValue(configJson.toString(), PostgreSQLConfig.class); - config.validate(); + config.validateAndNormalise(); return config; } diff --git a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java index 7d043d98..19ff78e8 100644 --- a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java +++ b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java @@ -23,66 +23,95 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import io.supertokens.pluginInterface.exceptions.InvalidConfigException; +import io.supertokens.storage.postgresql.annotations.ConnectionPoolProperty; +import io.supertokens.storage.postgresql.annotations.IgnoreForAnnotationCheck; +import io.supertokens.storage.postgresql.annotations.NotConflictingWithinUserPool; +import io.supertokens.storage.postgresql.annotations.UserPoolProperty; +import java.lang.reflect.Field; import java.net.URI; import java.util.HashSet; import java.util.Map; +import java.util.Objects; import java.util.Set; @JsonIgnoreProperties(ignoreUnknown = true) public class PostgreSQLConfig { @JsonProperty + @IgnoreForAnnotationCheck private int postgresql_config_version = -1; @JsonProperty + @ConnectionPoolProperty private int postgresql_connection_pool_size = 10; @JsonProperty + @UserPoolProperty private String postgresql_host = null; @JsonProperty + @UserPoolProperty private int postgresql_port = -1; @JsonProperty + @ConnectionPoolProperty private String postgresql_user = null; @JsonProperty + @ConnectionPoolProperty private String postgresql_password = null; @JsonProperty + @UserPoolProperty private String postgresql_database_name = null; @JsonProperty + @NotConflictingWithinUserPool private String postgresql_key_value_table_name = null; @JsonProperty + @NotConflictingWithinUserPool private String postgresql_session_info_table_name = null; @JsonProperty + @NotConflictingWithinUserPool private String postgresql_emailpassword_users_table_name = null; @JsonProperty + @NotConflictingWithinUserPool private String postgresql_emailpassword_pswd_reset_tokens_table_name = null; @JsonProperty + @NotConflictingWithinUserPool private String postgresql_emailverification_tokens_table_name = null; @JsonProperty + @NotConflictingWithinUserPool private String postgresql_emailverification_verified_emails_table_name = null; @JsonProperty + @NotConflictingWithinUserPool private String postgresql_thirdparty_users_table_name = null; @JsonProperty + @NotConflictingWithinUserPool private String postgresql_table_names_prefix = ""; @JsonProperty + @UserPoolProperty private String postgresql_table_schema = "public"; @JsonProperty + @IgnoreForAnnotationCheck private String postgresql_connection_uri = null; + @ConnectionPoolProperty + private String postgresql_connection_attributes = "allowPublicKeyRetrieval=true"; + + @ConnectionPoolProperty + private String postgresql_connection_scheme = "postgresql"; + public static Set getValidFields() { PostgreSQLConfig config = new PostgreSQLConfig(); JsonObject configObj = new GsonBuilder().serializeNulls().create().toJsonTree(config).getAsJsonObject(); @@ -95,17 +124,6 @@ public static Set getValidFields() { } public String getTableSchema() { - if (postgresql_connection_uri != null) { - String connectionAttributes = getConnectionAttributes(); - if (connectionAttributes.contains("currentSchema=")) { - String[] splitted = connectionAttributes.split("currentSchema="); - String valueStr = splitted[1]; - if (valueStr.contains("&")) { - return valueStr.split("&")[0]; - } - return valueStr.trim(); - } - } return postgresql_table_schema.trim(); } @@ -114,43 +132,14 @@ public int getConnectionPoolSize() { } public String getConnectionScheme() { - if (postgresql_connection_uri != null) { - URI uri = URI.create(postgresql_connection_uri); - - // sometimes if the scheme is missing, the host is returned as the scheme. To - // prevent that, - // we have a check - String host = this.getHostName(); - if (uri.getScheme() != null && !uri.getScheme().equals(host)) { - return uri.getScheme(); - } - } - return "postgresql"; + return postgresql_connection_scheme; } public String getConnectionAttributes() { - if (postgresql_connection_uri != null) { - URI uri = URI.create(postgresql_connection_uri); - String query = uri.getQuery(); - if (query != null) { - if (query.contains("allowPublicKeyRetrieval=")) { - return query; - } else { - return query + "&allowPublicKeyRetrieval=true"; - } - } - } - return "allowPublicKeyRetrieval=true"; + return postgresql_connection_attributes; } public String getHostName() { - if (postgresql_connection_uri != null) { - URI uri = URI.create(postgresql_connection_uri); - if (uri.getHost() != null) { - return uri.getHost(); - } - } - if (postgresql_host != null) { return postgresql_host; } @@ -158,13 +147,6 @@ public String getHostName() { } public int getPort() { - if (postgresql_connection_uri != null) { - URI uri = URI.create(postgresql_connection_uri); - if (uri.getPort() > 0) { - return uri.getPort(); - } - } - if (postgresql_port != -1) { return postgresql_port; } @@ -172,17 +154,6 @@ public int getPort() { } public String getUser() { - if (postgresql_connection_uri != null) { - URI uri = URI.create(postgresql_connection_uri); - String userInfo = uri.getUserInfo(); - if (userInfo != null) { - String[] userInfoArray = userInfo.split(":"); - if (userInfoArray.length > 0 && !userInfoArray[0].equals("")) { - return userInfoArray[0]; - } - } - } - if (postgresql_user != null) { return postgresql_user; } @@ -190,17 +161,6 @@ public String getUser() { } public String getPassword() { - if (postgresql_connection_uri != null) { - URI uri = URI.create(postgresql_connection_uri); - String userInfo = uri.getUserInfo(); - if (userInfo != null) { - String[] userInfoArray = userInfo.split(":"); - if (userInfoArray.length > 1 && !userInfoArray[1].equals("")) { - return userInfoArray[1]; - } - } - } - if (postgresql_password != null) { return postgresql_password; } @@ -208,17 +168,6 @@ public String getPassword() { } public String getDatabaseName() { - if (postgresql_connection_uri != null) { - URI uri = URI.create(postgresql_connection_uri); - String path = uri.getPath(); - if (path != null && !path.equals("") && !path.equals("/")) { - if (path.startsWith("/")) { - return path.substring(1); - } - return path; - } - } - if (postgresql_database_name != null) { return postgresql_database_name; } @@ -421,7 +370,7 @@ private String addSchemaToTableName(String tableName) { return name; } - void validate() throws InvalidConfigException { + void validateAndNormalise() throws InvalidConfigException { if (postgresql_connection_uri != null) { try { URI ignored = URI.create(postgresql_connection_uri); @@ -442,57 +391,144 @@ void validate() throws InvalidConfigException { throw new InvalidConfigException( "'postgresql_connection_pool_size' in the config.yaml file must be > 0"); } - } - void assertThatConfigFromSameUserPoolIsNotConflicting(PostgreSQLConfig otherConfig) throws InvalidConfigException { - if (!otherConfig.getTablePrefix().equals(getTablePrefix())) { - throw new InvalidConfigException( - "You cannot set different name for table prefix for the same user pool"); - } + // Normalisation + if (postgresql_connection_uri != null) { + { + URI uri = URI.create(postgresql_connection_uri); + String query = uri.getQuery(); + if (query != null) { + if (query.contains("allowPublicKeyRetrieval=")) { + postgresql_connection_attributes = query; + } else { + postgresql_connection_attributes = query + "&allowPublicKeyRetrieval=true"; + } + } + } - if (!otherConfig.getKeyValueTable().equals(getKeyValueTable())) { - throw new InvalidConfigException( - "You cannot set different name for table " + getKeyValueTable() + - " for the same user pool"); - } - if (!otherConfig.getSessionInfoTable().equals(getSessionInfoTable())) { - throw new InvalidConfigException( - "You cannot set different name for table " + getSessionInfoTable() + - " for the same user pool"); - } + { + String connectionAttributes = postgresql_connection_attributes; + if (connectionAttributes.contains("currentSchema=")) { + String[] splitted = connectionAttributes.split("currentSchema="); + String valueStr = splitted[1]; + if (valueStr.contains("&")) { + postgresql_table_schema = valueStr.split("&")[0]; + } else { + postgresql_table_schema = valueStr; + } + postgresql_table_schema = postgresql_table_schema.trim(); + } + } - if (!otherConfig.getEmailPasswordUsersTable().equals(getEmailPasswordUsersTable())) { - throw new InvalidConfigException( - "You cannot set different name for table " + getEmailPasswordUsersTable() + - " for the same user pool"); - } + { + URI uri = URI.create(postgresql_connection_uri); - if (!otherConfig.getPasswordResetTokensTable().equals( - getPasswordResetTokensTable())) { - throw new InvalidConfigException( - "You cannot set different name for table " + getPasswordResetTokensTable() + - " for the same user pool"); + // sometimes if the scheme is missing, the host is returned as the scheme. To + // prevent that, + // we have a check + String host = this.getHostName(); + if (uri.getScheme() != null && !uri.getScheme().equals(host)) { + postgresql_connection_scheme = uri.getScheme(); + } + } + + { + URI uri = URI.create(postgresql_connection_uri); + if (uri.getHost() != null) { + postgresql_host = uri.getHost(); + } + } + + { + URI uri = URI.create(postgresql_connection_uri); + if (uri.getPort() > 0) { + postgresql_port = uri.getPort(); + } + } + + { + URI uri = URI.create(postgresql_connection_uri); + String userInfo = uri.getUserInfo(); + if (userInfo != null) { + String[] userInfoArray = userInfo.split(":"); + if (userInfoArray.length > 0 && !userInfoArray[0].equals("")) { + postgresql_user = userInfoArray[0]; + } + } + } + + { + URI uri = URI.create(postgresql_connection_uri); + String userInfo = uri.getUserInfo(); + if (userInfo != null) { + String[] userInfoArray = userInfo.split(":"); + if (userInfoArray.length > 1 && !userInfoArray[1].equals("")) { + postgresql_password = userInfoArray[1]; + } + } + } + + { + URI uri = URI.create(postgresql_connection_uri); + String path = uri.getPath(); + if (path != null && !path.equals("") && !path.equals("/")) { + if (path.startsWith("/")) { + postgresql_database_name = path.substring(1); + } else { + postgresql_database_name = path; + } + } + } } + } - if (!otherConfig.getEmailVerificationTokensTable().equals( - getEmailVerificationTokensTable())) { - throw new InvalidConfigException( - "You cannot set different name for table " + getEmailVerificationTokensTable() + - " for the same user pool"); + void assertThatConfigFromSameUserPoolIsNotConflicting(PostgreSQLConfig otherConfig) throws InvalidConfigException { + for (Field field : PostgreSQLConfig.class.getDeclaredFields()) { + if (field.isAnnotationPresent(NotConflictingWithinUserPool.class)) { + try { + if (!Objects.equals(field.get(this), field.get(otherConfig))) { + throw new InvalidConfigException( + "You cannot set different values for " + field.getName() + + " for the same user pool"); + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } } + } - if (!otherConfig.getEmailVerificationTable().equals( - getEmailVerificationTable())) { - throw new InvalidConfigException( - "You cannot set different name for table " + - getEmailVerificationTable() + - " for the same user pool"); + public String getUserPoolId() { + StringBuilder userPoolId = new StringBuilder(); + for (Field field : PostgreSQLConfig.class.getDeclaredFields()) { + if (field.isAnnotationPresent(UserPoolProperty.class)) { + userPoolId.append("|"); + try { + if (field.get(this) != null) { + userPoolId.append(field.get(this).toString()); + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } } + return userPoolId.toString(); + } - if (!otherConfig.getThirdPartyUsersTable().equals(getThirdPartyUsersTable())) { - throw new InvalidConfigException( - "You cannot set different name for table " + getThirdPartyUsersTable() + - " for the same user pool"); + public String getConnectionPoolId() { + StringBuilder connectionPoolId = new StringBuilder(); + for (Field field : PostgreSQLConfig.class.getDeclaredFields()) { + if (field.isAnnotationPresent(ConnectionPoolProperty.class)) { + connectionPoolId.append("|"); + try { + if (field.get(this) != null) { + connectionPoolId.append(field.get(this).toString()); + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } } + return connectionPoolId.toString(); } } diff --git a/src/test/java/io/supertokens/storage/postgresql/test/ConfigTest.java b/src/test/java/io/supertokens/storage/postgresql/test/ConfigTest.java index e25afc18..4ef37312 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/ConfigTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/ConfigTest.java @@ -27,6 +27,10 @@ import io.supertokens.session.info.SessionInformationHolder; import io.supertokens.storage.postgresql.ConnectionPoolTestContent; import io.supertokens.storage.postgresql.Start; +import io.supertokens.storage.postgresql.annotations.ConnectionPoolProperty; +import io.supertokens.storage.postgresql.annotations.IgnoreForAnnotationCheck; +import io.supertokens.storage.postgresql.annotations.NotConflictingWithinUserPool; +import io.supertokens.storage.postgresql.annotations.UserPoolProperty; import io.supertokens.storage.postgresql.config.Config; import io.supertokens.storage.postgresql.config.PostgreSQLConfig; import io.supertokens.storageLayer.StorageLayer; @@ -39,9 +43,9 @@ import java.io.File; import java.io.IOException; +import java.lang.reflect.Field; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.*; public class ConfigTest { @@ -645,6 +649,20 @@ public void testValidConnectionURIAttributes() throws Exception { } } + @Test + public void testAllConfigsHaveAnAnnotation() throws Exception { + for (Field field : PostgreSQLConfig.class.getDeclaredFields()) { + if (field.isAnnotationPresent(IgnoreForAnnotationCheck.class)) { + continue; + } + + if (!(field.isAnnotationPresent(UserPoolProperty.class) || field.isAnnotationPresent(ConnectionPoolProperty.class) || field.isAnnotationPresent( + NotConflictingWithinUserPool.class))) { + fail(field.getName() + " does not have UserPoolProperty, ConnectionPoolProperty or NotConflictingWithinUserPool annotation"); + } + } + } + public static void checkConfig(PostgreSQLConfig config) throws IOException { final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); PostgreSQLConfig userConfig = mapper.readValue(new File("../config.yaml"), PostgreSQLConfig.class); diff --git a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java index 797ccbdb..9ff7ab2c 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java @@ -321,7 +321,7 @@ public void mergingTenantWithBaseConfigWithConflictingConfigsThrowsError() fail(); } catch (InvalidConfigException e) { assertEquals(e.getMessage(), - "You cannot set different name for table random for the same user pool"); + "You cannot set different values for postgresql_thirdparty_users_table_name for the same user pool"); } process.kill(); @@ -355,7 +355,7 @@ public void mergingDifferentConnectionPoolIdTenantWithBaseConfigWithConflictingC fail(); } catch (InvalidConfigException e) { assertEquals(e.getMessage(), - "You cannot set different name for table prefix for the same user pool"); + "You cannot set different values for postgresql_table_names_prefix for the same user pool"); } process.kill(); @@ -785,7 +785,7 @@ public void testTenantCreationAndThenDbDownDbThrowsErrorInRecipesAndDoesntAffect tenantConfigJson); StorageLayer.getMultitenancyStorage(process.getProcess()).createTenant(tenantConfig); - MultitenancyHelper.getInstance(process.getProcess()).refreshTenantsInCoreIfRequired(true); + MultitenancyHelper.getInstance(process.getProcess()).refreshTenantsInCoreBasedOnChangesInCoreConfigOrIfTenantListChanged(true); try { EmailPassword.signIn(tid.withStorage(StorageLayer.getStorage(tid, process.getProcess())), From a153063d30b9bfd78f4f72542f66748de64d6cd5 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Mon, 15 May 2023 12:39:03 +0530 Subject: [PATCH 081/106] fix: added setLogLevels (#103) --- src/main/java/io/supertokens/storage/postgresql/Start.java | 5 +++++ .../io/supertokens/storage/postgresql/config/Config.java | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 6d30988a..936dd8c0 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -2743,4 +2743,9 @@ public int removeExpiredCodes(TenantIdentifier tenantIdentifier, long expiredBef public Set getValidFieldsInConfig() { return PostgreSQLConfig.getValidFields(); } + + @Override + public void setLogLevels(Set logLevels) { + Config.setLogLevels(this, logLevels); + } } diff --git a/src/main/java/io/supertokens/storage/postgresql/config/Config.java b/src/main/java/io/supertokens/storage/postgresql/config/Config.java index e6da737d..20c55bef 100644 --- a/src/main/java/io/supertokens/storage/postgresql/config/Config.java +++ b/src/main/java/io/supertokens/storage/postgresql/config/Config.java @@ -36,7 +36,7 @@ public class Config extends ResourceDistributor.SingletonResource { private static final String RESOURCE_KEY = "io.supertokens.storage.postgresql.config.Config"; private final PostgreSQLConfig config; private final Start start; - private final Set logLevels; + private Set logLevels; private Config(Start start, JsonObject configJson, Set logLevels) throws InvalidConfigException { this.start = start; @@ -92,6 +92,10 @@ public static Set getLogLevels(Start start) { return getInstance(start).logLevels; } + public static void setLogLevels(Start start, Set logLevels) { + getInstance(start).logLevels = logLevels; + } + private PostgreSQLConfig loadPostgreSQLConfig(JsonObject configJson) throws IOException, InvalidConfigException { final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); PostgreSQLConfig config = mapper.readValue(configJson.toString(), PostgreSQLConfig.class); From 1b841e96f334116b2056516ac64d3eeed7821523 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Mon, 15 May 2023 13:16:28 +0530 Subject: [PATCH 082/106] fix: merge issue --- .../supertokens/storage/postgresql/Start.java | 139 +----------------- 1 file changed, 1 insertion(+), 138 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index dd7d7174..d7a28605 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -705,7 +705,7 @@ public boolean isUserIdBeingUsedInNonAuthRecipe(AppIdentifier appIdentifier, Str } } else if (className.equals(TOTPStorage.class.getName())) { try { - TOTPDevice[] devices = TOTPQueries.getDevices(this, userId); + TOTPDevice[] devices = TOTPQueries.getDevices(this, appIdentifier, userId); return devices.length > 0; } catch (SQLException e) { throw new StorageQueryException(e); @@ -2756,141 +2756,4 @@ public Set getValidFieldsInConfig() { public void setLogLevels(Set logLevels) { Config.setLogLevels(this, logLevels); } - - // TOTP recipe: - @Override - public void createDevice(TOTPDevice device) throws StorageQueryException, DeviceAlreadyExistsException { - try { - TOTPQueries.createDevice(this, device); - } catch (StorageTransactionLogicException e) { - Exception actualException = e.actualException; - - if (actualException instanceof PSQLException) { - ServerErrorMessage errMsg = ((PSQLException) actualException).getServerErrorMessage(); - - if (isPrimaryKeyError(errMsg, Config.getConfig(this).getTotpUserDevicesTable())) { - throw new DeviceAlreadyExistsException(); - } - } - - throw new StorageQueryException(e.actualException); - } - } - - @Override - public void markDeviceAsVerified(String userId, String deviceName) - throws StorageQueryException, UnknownDeviceException { - try { - int matchedCount = TOTPQueries.markDeviceAsVerified(this, userId, deviceName); - if (matchedCount == 0) { - // Note matchedCount != updatedCount - throw new UnknownDeviceException(); - } - return; // Device was marked as verified - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - - @Override - public int deleteDevice_Transaction(TransactionConnection con, String userId, String deviceName) - throws StorageQueryException { - Connection sqlCon = (Connection) con.getConnection(); - try { - return TOTPQueries.deleteDevice_Transaction(this, sqlCon, userId, deviceName); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - - @Override - public void removeUser_Transaction(TransactionConnection con, String userId) - throws StorageQueryException { - Connection sqlCon = (Connection) con.getConnection(); - try { - TOTPQueries.removeUser_Transaction(this, sqlCon, userId); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - - @Override - public void updateDeviceName(String userId, String oldDeviceName, String newDeviceName) - throws StorageQueryException, DeviceAlreadyExistsException, - UnknownDeviceException { - try { - int updatedCount = TOTPQueries.updateDeviceName(this, userId, oldDeviceName, newDeviceName); - if (updatedCount == 0) { - throw new UnknownDeviceException(); - } - } catch (SQLException e) { - if (e instanceof PSQLException) { - ServerErrorMessage errMsg = ((PSQLException) e).getServerErrorMessage(); - if (isPrimaryKeyError(errMsg, Config.getConfig(this).getTotpUserDevicesTable())) { - throw new DeviceAlreadyExistsException(); - } - } - } - } - - @Override - public TOTPDevice[] getDevices(String userId) - throws StorageQueryException { - try { - return TOTPQueries.getDevices(this, userId); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - - @Override - public TOTPDevice[] getDevices_Transaction(TransactionConnection con, String userId) - throws StorageQueryException { - Connection sqlCon = (Connection) con.getConnection(); - try { - return TOTPQueries.getDevices_Transaction(this, sqlCon, userId); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - - @Override - public void insertUsedCode_Transaction(TransactionConnection con, TOTPUsedCode usedCodeObj) - throws StorageQueryException, TotpNotEnabledException, UsedCodeAlreadyExistsException { - Connection sqlCon = (Connection) con.getConnection(); - try { - TOTPQueries.insertUsedCode_Transaction(this, sqlCon, usedCodeObj); - } catch (SQLException e) { - ServerErrorMessage err = ((PSQLException) e).getServerErrorMessage(); - - if (isPrimaryKeyError(err, Config.getConfig(this).getTotpUsedCodesTable())) { - throw new UsedCodeAlreadyExistsException(); - } else if (isForeignKeyConstraintError(err, Config.getConfig(this).getTotpUsedCodesTable(), "user_id")) { - throw new TotpNotEnabledException(); - } - - throw new StorageQueryException(e); - } - } - - @Override - public TOTPUsedCode[] getAllUsedCodesDescOrder_Transaction(TransactionConnection con, String userId) - throws StorageQueryException { - Connection sqlCon = (Connection) con.getConnection(); - try { - return TOTPQueries.getAllUsedCodesDescOrder_Transaction(this, sqlCon, userId); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - - @Override - public int removeExpiredCodes(long expiredBefore) - throws StorageQueryException { - try { - return TOTPQueries.removeExpiredCodes(this, expiredBefore); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } } From 1da59bc17cadede6c68247d7cfdb00429d81b3fa Mon Sep 17 00:00:00 2001 From: KShivendu Date: Mon, 15 May 2023 18:44:32 +0530 Subject: [PATCH 083/106] Overload deleteMfaInfoForUser and set factor column size to 64 --- src/main/java/io/supertokens/storage/postgresql/Start.java | 6 +++--- .../supertokens/storage/postgresql/queries/MfaQueries.java | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 5b7b20be..a5bc6578 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -2792,7 +2792,7 @@ public boolean disableFactor(TenantIdentifier tenantIdentifier, String userId, S } @Override - public boolean deleteUser(AppIdentifier appIdentifier, String userId) throws StorageQueryException { + public boolean deleteMfaInfoForUser(AppIdentifier appIdentifier, String userId) throws StorageQueryException { try { int deletedCount = MfaQueries.deleteUser(this, appIdentifier, userId); if (deletedCount == 0) { @@ -2805,9 +2805,9 @@ public boolean deleteUser(AppIdentifier appIdentifier, String userId) throws Sto } @Override - public boolean deleteUserFromTenant(TenantIdentifier tenantIdentifier, String userId) throws StorageQueryException { + public boolean deleteMfaInfoForUser(TenantIdentifier tenantIdentifier, String userId) throws StorageQueryException { try { - int deletedCount = MfaQueries.deleteUserFromTenant(this, tenantIdentifier, userId); + int deletedCount = MfaQueries.deleteUser(this, tenantIdentifier, userId); if (deletedCount == 0) { return false; } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java index 1e856fa1..ca966a21 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java @@ -35,7 +35,7 @@ public static String getQueryToCreateUserFactorsTable(Start start) { + "app_id VARCHAR(64) DEFAULT 'public'," + "tenant_id VARCHAR(64) DEFAULT 'public'," + "user_id VARCHAR(128) NOT NULL," - + "factor_id VARCHAR(16) NOT NULL," + + "factor_id VARCHAR(64) NOT NULL," + "PRIMARY KEY (app_id, tenant_id, user_id, factor_id)," + "FOREIGN KEY (app_id, tenant_id)" + "REFERENCES " + Config.getConfig(start).getTenantsTable() + " (app_id, tenant_id) ON DELETE CASCADE);"; @@ -95,7 +95,7 @@ public static int deleteUser(Start start, AppIdentifier appIdentifier, String us }); } - public static int deleteUserFromTenant(Start start, TenantIdentifier tenantIdentifier, String userId) + public static int deleteUser(Start start, TenantIdentifier tenantIdentifier, String userId) throws StorageQueryException, SQLException { String QUERY = "DELETE FROM " + Config.getConfig(start).getMfaUserFactorsTable() + " WHERE app_id = ? AND tenant_id = ? AND user_id = ?"; From f2616888f722f3ac22a086723b468ab20a797967 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Wed, 17 May 2023 15:30:50 +0530 Subject: [PATCH 084/106] fix: fkey names (#104) * fix: fixed fkey names on user tables * fix: catching fkey constraints * fix: added comments --- .../io/supertokens/storage/postgresql/Start.java | 12 ++++++++++++ .../postgresql/queries/EmailPasswordQueries.java | 2 +- .../postgresql/queries/PasswordlessQueries.java | 2 +- .../postgresql/queries/ThirdPartyQueries.java | 2 +- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index d7a28605..cd0375eb 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -831,6 +831,9 @@ public UserInfo signUp(TenantIdentifier tenantIdentifier, String id, String emai || isPrimaryKeyError(serverMessage, config.getEmailPasswordUserToTenantTable()) || isPrimaryKeyError(serverMessage, config.getAppIdToUserIdTable())) { throw new DuplicateUserIdException(); + } else if (isForeignKeyConstraintError(serverMessage, config.getEmailPasswordUsersTable(), "user_id")) { + // This should never happen because we add the user to app_id_to_user_id table first + throw new IllegalStateException("should never come here"); } else if (isForeignKeyConstraintError(serverMessage, config.getAppIdToUserIdTable(), "app_id")) { throw new TenantOrAppNotFoundException(tenantIdentifier.toAppIdentifier()); } else if (isForeignKeyConstraintError(serverMessage, config.getUsersTable(), "tenant_id")) { @@ -1204,6 +1207,10 @@ public io.supertokens.pluginInterface.thirdparty.UserInfo signUp( || isPrimaryKeyError(serverMessage, config.getAppIdToUserIdTable())) { throw new io.supertokens.pluginInterface.thirdparty.exception.DuplicateUserIdException(); + } else if (isForeignKeyConstraintError(serverMessage, config.getThirdPartyUsersTable(), "user_id")) { + // This should never happen because we add the user to app_id_to_user_id table first + throw new IllegalStateException("should never come here"); + } else if (isForeignKeyConstraintError(serverMessage, config.getAppIdToUserIdTable(), "app_id")) { throw new TenantOrAppNotFoundException(tenantIdentifier.toAppIdentifier()); @@ -1699,6 +1706,11 @@ public io.supertokens.pluginInterface.passwordless.UserInfo createUser(TenantIde throw new DuplicatePhoneNumberException(); } + if (isForeignKeyConstraintError(serverMessage, config.getPasswordlessUsersTable(), "user_id")) { + // This should never happen because we add the user to app_id_to_user_id table first + throw new IllegalStateException("should never come here"); + } + if (isForeignKeyConstraintError(serverMessage, config.getAppIdToUserIdTable(), "app_id")) { throw new TenantOrAppNotFoundException(tenantIdentifier.toAppIdentifier()); } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java index 77451cfa..9b28c672 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java @@ -49,7 +49,7 @@ static String getQueryToCreateUsersTable(Start start) { + "user_id CHAR(36) NOT NULL," + "email VARCHAR(256) NOT NULL," + "password_hash VARCHAR(256) NOT NULL," + "time_joined BIGINT NOT NULL," - + "CONSTRAINT " + Utils.getConstraintName(schema, emailPasswordUsersTable, "app_id", "fkey") + + "CONSTRAINT " + Utils.getConstraintName(schema, emailPasswordUsersTable, "user_id", "fkey") + " FOREIGN KEY(app_id, user_id)" + " REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable() + " (app_id, user_id) ON DELETE CASCADE," + "CONSTRAINT " + Utils.getConstraintName(schema, emailPasswordUsersTable, null, "pkey") diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java index 7023b7eb..4e3efa77 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java @@ -53,7 +53,7 @@ public static String getQueryToCreateUsersTable(Start start) { + "email VARCHAR(256)," + "phone_number VARCHAR(256)," + "time_joined BIGINT NOT NULL, " - + "CONSTRAINT " + Utils.getConstraintName(schema, usersTable, "app_id", "fkey") + + "CONSTRAINT " + Utils.getConstraintName(schema, usersTable, "user_id", "fkey") + " FOREIGN KEY(app_id, user_id)" + " REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable() + " (app_id, user_id) ON DELETE CASCADE," + "CONSTRAINT " + Utils.getConstraintName(schema, usersTable, null, "pkey") diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/ThirdPartyQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/ThirdPartyQueries.java index e056d93f..dc56177f 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/ThirdPartyQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/ThirdPartyQueries.java @@ -51,7 +51,7 @@ static String getQueryToCreateUsersTable(Start start) { + "user_id CHAR(36) NOT NULL," + "email VARCHAR(256) NOT NULL," + "time_joined BIGINT NOT NULL," - + "CONSTRAINT " + Utils.getConstraintName(schema, thirdPartyUsersTable, "app_id", "fkey") + + "CONSTRAINT " + Utils.getConstraintName(schema, thirdPartyUsersTable, "user_id", "fkey") + " FOREIGN KEY(app_id, user_id)" + " REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable() + " (app_id, user_id) ON DELETE CASCADE," + "CONSTRAINT " + Utils.getConstraintName(schema, thirdPartyUsersTable, null, "pkey") From 42eb27ec3b0ec6f3276e5ae92e5d25465df5429c Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Tue, 23 May 2023 11:22:20 +0530 Subject: [PATCH 085/106] fix: Postgres migration (#105) * fix: fixed fkey names on user tables * fix: catching fkey constraints * fix: added comments * fix: changelog * fix: changelog * fix: pr comment --- CHANGELOG.md | 715 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 715 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9989f50d..98782ed0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,721 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Changes + +- Support for multitenancy + - New tables `apps` and `tenants` have been added. + - Schema of tables have been changed, adding `app_id` and `tenant_id` columns in tables and constraints & indexes have been modified to include this columns. + - New user tables have been added to map users to apps and tenants. + - New tables for multitenancy have been added. + +### Migration + +Ensure the core is already migrated to version 2.21 and then, +Run the following: + +```sql +-- General Tables + +CREATE TABLE IF NOT EXISTS apps ( + app_id VARCHAR(64) NOT NULL DEFAULT 'public', + created_at_time BIGINT, + CONSTRAINT apps_pkey PRIMARY KEY(app_id) +); + +INSERT INTO apps (app_id, created_at_time) + VALUES ('public', 0); + +------------------------------------------------------------ + +CREATE TABLE IF NOT EXISTS tenants ( + app_id VARCHAR(64) NOT NULL DEFAULT 'public', + tenant_id VARCHAR(64) NOT NULL DEFAULT 'public', + created_at_time BIGINT , + CONSTRAINT tenants_pkey + PRIMARY KEY (app_id, tenant_id), + CONSTRAINT tenants_app_id_fkey FOREIGN KEY(app_id) + REFERENCES apps (app_id) ON DELETE CASCADE +); + +INSERT INTO tenants (app_id, tenant_id, created_at_time) + VALUES ('public', 'public', 0); + +------------------------------------------------------------ + +ALTER TABLE key_value + ADD COLUMN app_id VARCHAR(64) DEFAULT 'public', + ADD COLUMN tenant_id VARCHAR(64) DEFAULT 'public'; + +ALTER TABLE key_value + DROP CONSTRAINT key_value_pkey; + +ALTER TABLE key_value + ADD CONSTRAINT key_value_pkey + PRIMARY KEY (app_id, tenant_id, name); + +ALTER TABLE key_value + ADD CONSTRAINT key_value_tenant_id_fkey + FOREIGN KEY (app_id, tenant_id) + REFERENCES tenants (app_id, tenant_id) ON DELETE CASCADE; + +------------------------------------------------------------ + +CREATE TABLE IF NOT EXISTS app_id_to_user_id ( + app_id VARCHAR(64) NOT NULL DEFAULT 'public', + user_id CHAR(36) NOT NULL, + recipe_id VARCHAR(128) NOT NULL, + CONSTRAINT app_id_to_user_id_pkey + PRIMARY KEY (app_id, user_id), + CONSTRAINT app_id_to_user_id_app_id_fkey + FOREIGN KEY(app_id) REFERENCES apps (app_id) ON DELETE CASCADE +); + +INSERT INTO app_id_to_user_id (user_id, recipe_id) + SELECT user_id, recipe_id + FROM all_auth_recipe_users; + +------------------------------------------------------------ + +ALTER TABLE all_auth_recipe_users + ADD COLUMN app_id VARCHAR(64) DEFAULT 'public', + ADD COLUMN tenant_id VARCHAR(64) DEFAULT 'public'; + +ALTER TABLE all_auth_recipe_users + DROP CONSTRAINT all_auth_recipe_users_pkey CASCADE; + +ALTER TABLE all_auth_recipe_users + ADD CONSTRAINT all_auth_recipe_users_pkey + PRIMARY KEY (app_id, tenant_id, user_id); + +ALTER TABLE all_auth_recipe_users + ADD CONSTRAINT all_auth_recipe_users_tenant_id_fkey + FOREIGN KEY (app_id, tenant_id) + REFERENCES tenants (app_id, tenant_id) ON DELETE CASCADE; + +ALTER TABLE all_auth_recipe_users + ADD CONSTRAINT all_auth_recipe_users_user_id_fkey + FOREIGN KEY (app_id, user_id) + REFERENCES app_id_to_user_id (app_id, user_id) ON DELETE CASCADE; + +DROP INDEX all_auth_recipe_users_pagination_index; + +CREATE INDEX all_auth_recipe_users_pagination_index ON all_auth_recipe_users (time_joined DESC, user_id DESC, tenant_id DESC, app_id DESC); + +-- Multitenancy + +CREATE TABLE IF NOT EXISTS tenant_configs ( + connection_uri_domain VARCHAR(256) DEFAULT '', + app_id VARCHAR(64) DEFAULT 'public', + tenant_id VARCHAR(64) DEFAULT 'public', + core_config TEXT, + email_password_enabled BOOLEAN, + passwordless_enabled BOOLEAN, + third_party_enabled BOOLEAN, + CONSTRAINT tenant_configs_pkey + PRIMARY KEY (connection_uri_domain, app_id, tenant_id) +); + +------------------------------------------------------------ + +CREATE TABLE IF NOT EXISTS tenant_thirdparty_providers ( + connection_uri_domain VARCHAR(256) DEFAULT '', + app_id VARCHAR(64) DEFAULT 'public', + tenant_id VARCHAR(64) DEFAULT 'public', + third_party_id VARCHAR(28) NOT NULL, + name VARCHAR(64), + authorization_endpoint TEXT, + authorization_endpoint_query_params TEXT, + token_endpoint TEXT, + token_endpoint_body_params TEXT, + user_info_endpoint TEXT, + user_info_endpoint_query_params TEXT, + user_info_endpoint_headers TEXT, + jwks_uri TEXT, + oidc_discovery_endpoint TEXT, + require_email BOOLEAN, + user_info_map_from_id_token_payload_user_id VARCHAR(64), + user_info_map_from_id_token_payload_email VARCHAR(64), + user_info_map_from_id_token_payload_email_verified VARCHAR(64), + user_info_map_from_user_info_endpoint_user_id VARCHAR(64), + user_info_map_from_user_info_endpoint_email VARCHAR(64), + user_info_map_from_user_info_endpoint_email_verified VARCHAR(64), + CONSTRAINT tenant_thirdparty_providers_pkey + PRIMARY KEY (connection_uri_domain, app_id, tenant_id, third_party_id), + CONSTRAINT tenant_thirdparty_providers_tenant_id_fkey + FOREIGN KEY(connection_uri_domain, app_id, tenant_id) + REFERENCES tenant_configs (connection_uri_domain, app_id, tenant_id) ON DELETE CASCADE +); + +------------------------------------------------------------ + +CREATE TABLE IF NOT EXISTS tenant_thirdparty_provider_clients ( + connection_uri_domain VARCHAR(256) DEFAULT '', + app_id VARCHAR(64) DEFAULT 'public', + tenant_id VARCHAR(64) DEFAULT 'public', + third_party_id VARCHAR(28) NOT NULL, + client_type VARCHAR(64) NOT NULL DEFAULT '', + client_id VARCHAR(256) NOT NULL, + client_secret TEXT, + scope VARCHAR(128)[], + force_pkce BOOLEAN, + additional_config TEXT, + CONSTRAINT tenant_thirdparty_provider_clients_pkey + PRIMARY KEY (connection_uri_domain, app_id, tenant_id, third_party_id, client_type), + CONSTRAINT tenant_thirdparty_provider_clients_third_party_id_fkey + FOREIGN KEY (connection_uri_domain, app_id, tenant_id, third_party_id) + REFERENCES tenant_thirdparty_providers (connection_uri_domain, app_id, tenant_id, third_party_id) ON DELETE CASCADE +); + +-- Session + +ALTER TABLE session_info + ADD COLUMN app_id VARCHAR(64) DEFAULT 'public', + ADD COLUMN tenant_id VARCHAR(64) DEFAULT 'public'; + +ALTER TABLE session_info + DROP CONSTRAINT session_info_pkey CASCADE; + +ALTER TABLE session_info + ADD CONSTRAINT session_info_pkey + PRIMARY KEY (app_id, tenant_id, session_handle); + +ALTER TABLE session_info + ADD CONSTRAINT session_info_tenant_id_fkey + FOREIGN KEY (app_id, tenant_id) + REFERENCES tenants (app_id, tenant_id) ON DELETE CASCADE; + +CREATE INDEX session_expiry_index ON session_info (expires_at); + +------------------------------------------------------------ + +ALTER TABLE session_access_token_signing_keys + ADD COLUMN app_id VARCHAR(64) DEFAULT 'public'; + +ALTER TABLE session_access_token_signing_keys + DROP CONSTRAINT session_access_token_signing_keys_pkey CASCADE; + +ALTER TABLE session_access_token_signing_keys + ADD CONSTRAINT session_access_token_signing_keys_pkey + PRIMARY KEY (app_id, created_at_time); + +ALTER TABLE session_access_token_signing_keys + ADD CONSTRAINT session_access_token_signing_keys_app_id_fkey + FOREIGN KEY (app_id) + REFERENCES apps (app_id) ON DELETE CASCADE; + +-- JWT + +ALTER TABLE jwt_signing_keys + ADD COLUMN app_id VARCHAR(64) DEFAULT 'public'; + +ALTER TABLE jwt_signing_keys + DROP CONSTRAINT jwt_signing_keys_pkey CASCADE; + +ALTER TABLE jwt_signing_keys + ADD CONSTRAINT jwt_signing_keys_pkey + PRIMARY KEY (app_id, key_id); + +ALTER TABLE jwt_signing_keys + ADD CONSTRAINT jwt_signing_keys_app_id_fkey + FOREIGN KEY (app_id) + REFERENCES apps (app_id) ON DELETE CASCADE; + +-- EmailVerification + +ALTER TABLE emailverification_verified_emails + ADD COLUMN app_id VARCHAR(64) DEFAULT 'public'; + +ALTER TABLE emailverification_verified_emails + DROP CONSTRAINT emailverification_verified_emails_pkey CASCADE; + +ALTER TABLE emailverification_verified_emails + ADD CONSTRAINT emailverification_verified_emails_pkey + PRIMARY KEY (app_id, user_id, email); + +ALTER TABLE emailverification_verified_emails + ADD CONSTRAINT emailverification_verified_emails_app_id_fkey + FOREIGN KEY (app_id) + REFERENCES apps (app_id) ON DELETE CASCADE; + +------------------------------------------------------------ + +ALTER TABLE emailverification_tokens + ADD COLUMN app_id VARCHAR(64) DEFAULT 'public', + ADD COLUMN tenant_id VARCHAR(64) DEFAULT 'public'; + +ALTER TABLE emailverification_tokens + DROP CONSTRAINT emailverification_tokens_pkey CASCADE; + +ALTER TABLE emailverification_tokens + ADD CONSTRAINT emailverification_tokens_pkey + PRIMARY KEY (app_id, tenant_id, user_id, email, token); + +ALTER TABLE emailverification_tokens + ADD CONSTRAINT emailverification_tokens_tenant_id_fkey + FOREIGN KEY (app_id, tenant_id) + REFERENCES tenants (app_id, tenant_id) ON DELETE CASCADE; + + +-- EmailPassword + +ALTER TABLE emailpassword_users + ADD COLUMN app_id VARCHAR(64) DEFAULT 'public'; + +ALTER TABLE emailpassword_users + DROP CONSTRAINT emailpassword_users_pkey CASCADE; + +ALTER TABLE emailpassword_users + DROP CONSTRAINT emailpassword_users_email_key CASCADE; + +ALTER TABLE emailpassword_users + ADD CONSTRAINT emailpassword_users_pkey + PRIMARY KEY (app_id, user_id); + +ALTER TABLE emailpassword_users + ADD CONSTRAINT emailpassword_users_user_id_fkey + FOREIGN KEY (app_id, user_id) + REFERENCES app_id_to_user_id (app_id, user_id) ON DELETE CASCADE; + +------------------------------------------------------------ + +CREATE TABLE IF NOT EXISTS emailpassword_user_to_tenant ( + app_id VARCHAR(64) DEFAULT 'public', + tenant_id VARCHAR(64) DEFAULT 'public', + user_id CHAR(36) NOT NULL, + email VARCHAR(256) NOT NULL, + CONSTRAINT emailpassword_user_to_tenant_email_key + UNIQUE (app_id, tenant_id, email), + CONSTRAINT emailpassword_user_to_tenant_pkey + PRIMARY KEY (app_id, tenant_id, user_id), + CONSTRAINT emailpassword_user_to_tenant_user_id_fkey + FOREIGN KEY (app_id, tenant_id, user_id) + REFERENCES all_auth_recipe_users (app_id, tenant_id, user_id) ON DELETE CASCADE +); + +INSERT INTO emailpassword_user_to_tenant (user_id, email) + SELECT user_id, email FROM emailpassword_users; + +------------------------------------------------------------ + +ALTER TABLE emailpassword_pswd_reset_tokens + ADD COLUMN app_id VARCHAR(64) DEFAULT 'public'; + +ALTER TABLE emailpassword_pswd_reset_tokens + DROP CONSTRAINT emailpassword_pswd_reset_tokens_pkey CASCADE; + +ALTER TABLE emailpassword_pswd_reset_tokens + ADD CONSTRAINT emailpassword_pswd_reset_tokens_pkey + PRIMARY KEY (app_id, user_id, token); + +ALTER TABLE emailpassword_pswd_reset_tokens + DROP CONSTRAINT IF EXISTS emailpassword_pswd_reset_tokens_user_id_fkey; + +ALTER TABLE emailpassword_pswd_reset_tokens + ADD CONSTRAINT emailpassword_pswd_reset_tokens_user_id_fkey + FOREIGN KEY (app_id, user_id) + REFERENCES emailpassword_users (app_id, user_id) ON DELETE CASCADE; + +-- Passwordless + +ALTER TABLE passwordless_users + ADD COLUMN app_id VARCHAR(64) DEFAULT 'public'; + +ALTER TABLE passwordless_users + DROP CONSTRAINT passwordless_users_pkey CASCADE; + +ALTER TABLE passwordless_users + ADD CONSTRAINT passwordless_users_pkey + PRIMARY KEY (app_id, user_id); + +ALTER TABLE passwordless_users + DROP CONSTRAINT passwordless_users_email_key; + +ALTER TABLE passwordless_users + DROP CONSTRAINT passwordless_users_phone_number_key; + +ALTER TABLE passwordless_users + ADD CONSTRAINT passwordless_users_user_id_fkey + FOREIGN KEY (app_id, user_id) + REFERENCES app_id_to_user_id (app_id, user_id) ON DELETE CASCADE; + +------------------------------------------------------------ + +CREATE TABLE IF NOT EXISTS passwordless_user_to_tenant ( + app_id VARCHAR(64) DEFAULT 'public', + tenant_id VARCHAR(64) DEFAULT 'public', + user_id CHAR(36) NOT NULL, + email VARCHAR(256), + phone_number VARCHAR(256), + CONSTRAINT passwordless_user_to_tenant_email_key + UNIQUE (app_id, tenant_id, email), + CONSTRAINT passwordless_user_to_tenant_phone_number_key + UNIQUE (app_id, tenant_id, phone_number), + CONSTRAINT passwordless_user_to_tenant_pkey + PRIMARY KEY (app_id, tenant_id, user_id), + CONSTRAINT passwordless_user_to_tenant_user_id_fkey + FOREIGN KEY (app_id, tenant_id, user_id) + REFERENCES all_auth_recipe_users (app_id, tenant_id, user_id) ON DELETE CASCADE +); + +INSERT INTO passwordless_user_to_tenant (user_id, email, phone_number) + SELECT user_id, email, phone_number FROM passwordless_users; + +------------------------------------------------------------ + +ALTER TABLE passwordless_devices + ADD COLUMN app_id VARCHAR(64) DEFAULT 'public', + ADD COLUMN tenant_id VARCHAR(64) DEFAULT 'public'; + +ALTER TABLE passwordless_devices + DROP CONSTRAINT passwordless_devices_pkey CASCADE; + +ALTER TABLE passwordless_devices + ADD CONSTRAINT passwordless_devices_pkey + PRIMARY KEY (app_id, tenant_id, device_id_hash); + +ALTER TABLE passwordless_devices + ADD CONSTRAINT passwordless_devices_tenant_id_fkey + FOREIGN KEY (app_id, tenant_id) + REFERENCES tenants (app_id, tenant_id) ON DELETE CASCADE; + +DROP INDEX passwordless_devices_email_index; + +CREATE INDEX passwordless_devices_email_index ON passwordless_devices (app_id, tenant_id, email); + +DROP INDEX passwordless_devices_phone_number_index; + +CREATE INDEX passwordless_devices_phone_number_index ON passwordless_devices (app_id, tenant_id, phone_number); + +------------------------------------------------------------ + +ALTER TABLE passwordless_codes + ADD COLUMN app_id VARCHAR(64) DEFAULT 'public', + ADD COLUMN tenant_id VARCHAR(64) DEFAULT 'public'; + +ALTER TABLE passwordless_codes + DROP CONSTRAINT passwordless_codes_pkey CASCADE; + +ALTER TABLE passwordless_codes + ADD CONSTRAINT passwordless_codes_pkey + PRIMARY KEY (app_id, tenant_id, code_id); + +ALTER TABLE passwordless_codes + DROP CONSTRAINT IF EXISTS passwordless_codes_device_id_hash_fkey; + +ALTER TABLE passwordless_codes + ADD CONSTRAINT passwordless_codes_device_id_hash_fkey + FOREIGN KEY (app_id, tenant_id, device_id_hash) + REFERENCES passwordless_devices (app_id, tenant_id, device_id_hash) ON DELETE CASCADE; + +ALTER TABLE passwordless_codes + DROP CONSTRAINT passwordless_codes_link_code_hash_key; + +ALTER TABLE passwordless_codes + ADD CONSTRAINT passwordless_codes_link_code_hash_key + UNIQUE (app_id, tenant_id, link_code_hash); + +DROP INDEX passwordless_codes_created_at_index; + +CREATE INDEX passwordless_codes_created_at_index ON passwordless_codes (app_id, tenant_id, created_at); + +DROP INDEX passwordless_codes_device_id_hash_index; +CREATE INDEX passwordless_codes_device_id_hash_index ON passwordless_codes (app_id, tenant_id, device_id_hash); + +-- ThirdParty + +ALTER TABLE thirdparty_users + ADD COLUMN app_id VARCHAR(64) DEFAULT 'public'; + +ALTER TABLE thirdparty_users + DROP CONSTRAINT thirdparty_users_pkey CASCADE; + +ALTER TABLE thirdparty_users + DROP CONSTRAINT thirdparty_users_user_id_key CASCADE; + +ALTER TABLE thirdparty_users + ADD CONSTRAINT thirdparty_users_pkey + PRIMARY KEY (app_id, user_id); + +ALTER TABLE thirdparty_users + ADD CONSTRAINT thirdparty_users_user_id_fkey + FOREIGN KEY (app_id, user_id) + REFERENCES app_id_to_user_id (app_id, user_id) ON DELETE CASCADE; + +DROP INDEX IF EXISTS thirdparty_users_thirdparty_user_id_index; + +CREATE INDEX thirdparty_users_thirdparty_user_id_index ON thirdparty_users (app_id, third_party_id, third_party_user_id); + +DROP INDEX IF EXISTS thirdparty_users_email_index; + +CREATE INDEX thirdparty_users_email_index ON thirdparty_users (app_id, email); + +------------------------------------------------------------ + +CREATE TABLE IF NOT EXISTS thirdparty_user_to_tenant ( + app_id VARCHAR(64) DEFAULT 'public', + tenant_id VARCHAR(64) DEFAULT 'public', + user_id CHAR(36) NOT NULL, + third_party_id VARCHAR(28) NOT NULL, + third_party_user_id VARCHAR(256) NOT NULL, + CONSTRAINT thirdparty_user_to_tenant_third_party_user_id_key + UNIQUE (app_id, tenant_id, third_party_id, third_party_user_id), + CONSTRAINT thirdparty_user_to_tenant_pkey + PRIMARY KEY (app_id, tenant_id, user_id), + CONSTRAINT thirdparty_user_to_tenant_user_id_fkey + FOREIGN KEY (app_id, tenant_id, user_id) + REFERENCES all_auth_recipe_users (app_id, tenant_id, user_id) ON DELETE CASCADE +); + +INSERT INTO thirdparty_user_to_tenant (user_id, third_party_id, third_party_user_id) + SELECT user_id, third_party_id, third_party_user_id FROM thirdparty_users; + +-- UserIdMapping + +ALTER TABLE userid_mapping + ADD COLUMN app_id VARCHAR(64) DEFAULT 'public'; + +ALTER TABLE userid_mapping + DROP CONSTRAINT userid_mapping_pkey CASCADE; + +ALTER TABLE userid_mapping + ADD CONSTRAINT userid_mapping_pkey + PRIMARY KEY (app_id, supertokens_user_id, external_user_id); + +ALTER TABLE userid_mapping + DROP CONSTRAINT userid_mapping_supertokens_user_id_key; + +ALTER TABLE userid_mapping + ADD CONSTRAINT userid_mapping_supertokens_user_id_key + UNIQUE (app_id, supertokens_user_id); + +ALTER TABLE userid_mapping + DROP CONSTRAINT userid_mapping_external_user_id_key; + +ALTER TABLE userid_mapping + ADD CONSTRAINT userid_mapping_external_user_id_key + UNIQUE (app_id, external_user_id); + +ALTER TABLE userid_mapping + DROP CONSTRAINT IF EXISTS userid_mapping_supertokens_user_id_fkey; + +ALTER TABLE userid_mapping + ADD CONSTRAINT userid_mapping_supertokens_user_id_fkey + FOREIGN KEY (app_id, supertokens_user_id) + REFERENCES app_id_to_user_id (app_id, user_id) ON DELETE CASCADE; + +-- UserRoles + +ALTER TABLE roles + ADD COLUMN app_id VARCHAR(64) DEFAULT 'public'; + +ALTER TABLE roles + DROP CONSTRAINT roles_pkey CASCADE; + +ALTER TABLE roles + ADD CONSTRAINT roles_pkey + PRIMARY KEY (app_id, role); + +ALTER TABLE roles + ADD CONSTRAINT roles_app_id_fkey + FOREIGN KEY (app_id) + REFERENCES apps (app_id) ON DELETE CASCADE; + +------------------------------------------------------------ + +ALTER TABLE role_permissions + ADD COLUMN app_id VARCHAR(64) DEFAULT 'public'; + +ALTER TABLE role_permissions + DROP CONSTRAINT role_permissions_pkey CASCADE; + +ALTER TABLE role_permissions + ADD CONSTRAINT role_permissions_pkey + PRIMARY KEY (app_id, role, permission); + +ALTER TABLE role_permissions + DROP CONSTRAINT IF EXISTS role_permissions_role_fkey; + +ALTER TABLE role_permissions + ADD CONSTRAINT role_permissions_role_fkey + FOREIGN KEY (app_id, role) + REFERENCES roles (app_id, role) ON DELETE CASCADE; + +DROP INDEX role_permissions_permission_index; + +CREATE INDEX role_permissions_permission_index ON role_permissions (app_id, permission); + +------------------------------------------------------------ + +ALTER TABLE user_roles + ADD COLUMN app_id VARCHAR(64) DEFAULT 'public', + ADD COLUMN tenant_id VARCHAR(64) DEFAULT 'public'; + +ALTER TABLE user_roles + DROP CONSTRAINT user_roles_pkey CASCADE; + +ALTER TABLE user_roles + ADD CONSTRAINT user_roles_pkey + PRIMARY KEY (app_id, tenant_id, user_id, role); + +ALTER TABLE user_roles + ADD CONSTRAINT user_roles_tenant_id_fkey + FOREIGN KEY (app_id, tenant_id) + REFERENCES tenants (app_id, tenant_id) ON DELETE CASCADE; + +ALTER TABLE user_roles + DROP CONSTRAINT IF EXISTS user_roles_role_fkey; + +ALTER TABLE user_roles + ADD CONSTRAINT user_roles_role_fkey + FOREIGN KEY (app_id, role) + REFERENCES roles (app_id, role) ON DELETE CASCADE; + +DROP INDEX user_roles_role_index; + +CREATE INDEX user_roles_role_index ON user_roles (app_id, tenant_id, role); + +-- UserMetadata + +ALTER TABLE user_metadata + ADD COLUMN app_id VARCHAR(64) DEFAULT 'public'; + +ALTER TABLE user_metadata + DROP CONSTRAINT user_metadata_pkey CASCADE; + +ALTER TABLE user_metadata + ADD CONSTRAINT user_metadata_pkey + PRIMARY KEY (app_id, user_id); + +ALTER TABLE user_metadata + ADD CONSTRAINT user_metadata_app_id_fkey + FOREIGN KEY (app_id) + REFERENCES apps (app_id) ON DELETE CASCADE; + +-- Dashboard + +ALTER TABLE dashboard_users + ADD COLUMN app_id VARCHAR(64) DEFAULT 'public'; + +ALTER TABLE dashboard_users + DROP CONSTRAINT dashboard_users_pkey CASCADE; + +ALTER TABLE dashboard_users + ADD CONSTRAINT dashboard_users_pkey + PRIMARY KEY (app_id, user_id); + +ALTER TABLE dashboard_users + DROP CONSTRAINT dashboard_users_email_key; + +ALTER TABLE dashboard_users + ADD CONSTRAINT dashboard_users_email_key + UNIQUE (app_id, email); + +ALTER TABLE dashboard_users + ADD CONSTRAINT dashboard_users_app_id_fkey + FOREIGN KEY (app_id) + REFERENCES apps (app_id) ON DELETE CASCADE; + +------------------------------------------------------------ + +ALTER TABLE dashboard_user_sessions + ADD COLUMN app_id VARCHAR(64) DEFAULT 'public'; + +ALTER TABLE dashboard_user_sessions + DROP CONSTRAINT dashboard_user_sessions_pkey CASCADE; + +ALTER TABLE dashboard_user_sessions + ADD CONSTRAINT dashboard_user_sessions_pkey + PRIMARY KEY (app_id, session_id); + +ALTER TABLE dashboard_user_sessions + DROP CONSTRAINT IF EXISTS dashboard_user_sessions_user_id_fkey; + +ALTER TABLE dashboard_user_sessions + ADD CONSTRAINT dashboard_user_sessions_user_id_fkey + FOREIGN KEY (app_id, user_id) + REFERENCES dashboard_users (app_id, user_id) ON DELETE CASCADE; + +-- TOTP + +ALTER TABLE totp_users + ADD COLUMN app_id VARCHAR(64) DEFAULT 'public'; + +ALTER TABLE totp_users + DROP CONSTRAINT totp_users_pkey CASCADE; + +ALTER TABLE totp_users + ADD CONSTRAINT totp_users_pkey + PRIMARY KEY (app_id, user_id); + +ALTER TABLE totp_users + ADD CONSTRAINT totp_users_app_id_fkey + FOREIGN KEY (app_id) + REFERENCES apps (app_id) ON DELETE CASCADE; + +------------------------------------------------------------ + +ALTER TABLE totp_user_devices + ADD COLUMN app_id VARCHAR(64) DEFAULT 'public'; + +ALTER TABLE totp_user_devices + DROP CONSTRAINT totp_user_devices_pkey; + +ALTER TABLE totp_user_devices + ADD CONSTRAINT totp_user_devices_pkey + PRIMARY KEY (app_id, user_id, device_name); + +ALTER TABLE totp_user_devices + DROP CONSTRAINT IF EXISTS totp_user_devices_user_id_fkey; + +ALTER TABLE totp_user_devices + ADD CONSTRAINT totp_user_devices_user_id_fkey + FOREIGN KEY (app_id, user_id) + REFERENCES totp_users (app_id, user_id) ON DELETE CASCADE; + +------------------------------------------------------------ + +ALTER TABLE totp_used_codes + ADD COLUMN app_id VARCHAR(64) DEFAULT 'public', + ADD COLUMN tenant_id VARCHAR(64) DEFAULT 'public'; + +ALTER TABLE totp_used_codes + DROP CONSTRAINT totp_used_codes_pkey CASCADE; + +ALTER TABLE totp_used_codes + ADD CONSTRAINT totp_used_codes_pkey + PRIMARY KEY (app_id, tenant_id, user_id, created_time_ms); + +ALTER TABLE totp_used_codes + DROP CONSTRAINT IF EXISTS totp_used_codes_user_id_fkey; + +ALTER TABLE totp_used_codes + ADD CONSTRAINT totp_used_codes_user_id_fkey + FOREIGN KEY (app_id, user_id) + REFERENCES totp_users (app_id, user_id) ON DELETE CASCADE; + +ALTER TABLE totp_used_codes + ADD CONSTRAINT totp_used_codes_tenant_id_fkey + FOREIGN KEY (app_id, tenant_id) + REFERENCES tenants (app_id, tenant_id) ON DELETE CASCADE; + +DROP INDEX totp_used_codes_expiry_time_ms_index; + +CREATE INDEX totp_used_codes_expiry_time_ms_index ON totp_used_codes (app_id, tenant_id, expiry_time_ms); + +-- ActiveUsers + +ALTER TABLE user_last_active + ADD COLUMN app_id VARCHAR(64) DEFAULT 'public'; + +ALTER TABLE user_last_active + DROP CONSTRAINT user_last_active_pkey CASCADE; + +ALTER TABLE user_last_active + ADD CONSTRAINT user_last_active_pkey + PRIMARY KEY (app_id, user_id); +``` + ## [3.0.0] - 2023-04-05 - Adds `use_static_key` `BOOLEAN` column into `session_info` From e2b9ab3b40ac9b16c34a7c909a71af6f68af9d1c Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Thu, 25 May 2023 19:28:56 +0530 Subject: [PATCH 086/106] fix: Fkey indexes (#109) * fix: fkey indexes * fix: fixes * fix: active users storage stuff * fix: active users storage stuff * fix: fixed index name * fix: updated migration script --- CHANGELOG.md | 58 +++++++++++ .../supertokens/storage/postgresql/Start.java | 89 ++++++++--------- .../queries/ActiveUsersQueries.java | 48 ++++++++- .../postgresql/queries/DashboardQueries.java | 12 ++- .../queries/EmailPasswordQueries.java | 8 +- .../queries/EmailVerificationQueries.java | 10 ++ .../postgresql/queries/GeneralQueries.java | 98 ++++++++++++++++++- .../postgresql/queries/JWTSigningQueries.java | 5 + .../queries/MultitenancyQueries.java | 10 ++ .../queries/PasswordlessQueries.java | 5 + .../postgresql/queries/SessionQueries.java | 9 ++ .../postgresql/queries/TOTPQueries.java | 20 ++++ .../queries/UserIdMappingQueries.java | 6 ++ .../queries/UserMetadataQueries.java | 4 + .../postgresql/queries/UserRolesQueries.java | 20 +++- 15 files changed, 342 insertions(+), 60 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98782ed0..e125b695 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,8 @@ CREATE TABLE IF NOT EXISTS tenants ( INSERT INTO tenants (app_id, tenant_id, created_at_time) VALUES ('public', 'public', 0); +CREATE INDEX tenants_app_id_index ON tenants (app_id); + ------------------------------------------------------------ ALTER TABLE key_value @@ -65,6 +67,8 @@ ALTER TABLE key_value FOREIGN KEY (app_id, tenant_id) REFERENCES tenants (app_id, tenant_id) ON DELETE CASCADE; +CREATE INDEX key_value_tenant_id_index ON key_value (app_id, tenant_id); + ------------------------------------------------------------ CREATE TABLE IF NOT EXISTS app_id_to_user_id ( @@ -81,6 +85,8 @@ INSERT INTO app_id_to_user_id (user_id, recipe_id) SELECT user_id, recipe_id FROM all_auth_recipe_users; +CREATE INDEX app_id_to_user_id_app_id_index ON app_id_to_user_id (app_id); + ------------------------------------------------------------ ALTER TABLE all_auth_recipe_users @@ -108,6 +114,10 @@ DROP INDEX all_auth_recipe_users_pagination_index; CREATE INDEX all_auth_recipe_users_pagination_index ON all_auth_recipe_users (time_joined DESC, user_id DESC, tenant_id DESC, app_id DESC); +CREATE INDEX all_auth_recipe_user_id_index ON all_auth_recipe_users (app_id, user_id); + +CREATE INDEX all_auth_recipe_tenant_id_index ON all_auth_recipe_users (app_id, tenant_id); + -- Multitenancy CREATE TABLE IF NOT EXISTS tenant_configs ( @@ -153,6 +163,8 @@ CREATE TABLE IF NOT EXISTS tenant_thirdparty_providers ( REFERENCES tenant_configs (connection_uri_domain, app_id, tenant_id) ON DELETE CASCADE ); +CREATE INDEX tenant_thirdparty_providers_tenant_id_index ON tenant_thirdparty_providers (connection_uri_domain, app_id, tenant_id); + ------------------------------------------------------------ CREATE TABLE IF NOT EXISTS tenant_thirdparty_provider_clients ( @@ -173,6 +185,8 @@ CREATE TABLE IF NOT EXISTS tenant_thirdparty_provider_clients ( REFERENCES tenant_thirdparty_providers (connection_uri_domain, app_id, tenant_id, third_party_id) ON DELETE CASCADE ); +CREATE INDEX tenant_thirdparty_provider_clients_third_party_id_index ON tenant_thirdparty_provider_clients (connection_uri_domain, app_id, tenant_id, third_party_id); + -- Session ALTER TABLE session_info @@ -193,6 +207,8 @@ ALTER TABLE session_info CREATE INDEX session_expiry_index ON session_info (expires_at); +CREATE INDEX session_info_tenant_id_index ON session_info (app_id, tenant_id); + ------------------------------------------------------------ ALTER TABLE session_access_token_signing_keys @@ -210,6 +226,8 @@ ALTER TABLE session_access_token_signing_keys FOREIGN KEY (app_id) REFERENCES apps (app_id) ON DELETE CASCADE; +CREATE INDEX access_token_signing_keys_app_id_index ON session_access_token_signing_keys (app_id); + -- JWT ALTER TABLE jwt_signing_keys @@ -227,6 +245,8 @@ ALTER TABLE jwt_signing_keys FOREIGN KEY (app_id) REFERENCES apps (app_id) ON DELETE CASCADE; +CREATE INDEX jwt_signing_keys_app_id_index ON jwt_signing_keys (app_id); + -- EmailVerification ALTER TABLE emailverification_verified_emails @@ -244,6 +264,8 @@ ALTER TABLE emailverification_verified_emails FOREIGN KEY (app_id) REFERENCES apps (app_id) ON DELETE CASCADE; +CREATE INDEX emailverification_verified_emails_app_id_index ON emailverification_verified_emails (app_id); + ------------------------------------------------------------ ALTER TABLE emailverification_tokens @@ -262,6 +284,7 @@ ALTER TABLE emailverification_tokens FOREIGN KEY (app_id, tenant_id) REFERENCES tenants (app_id, tenant_id) ON DELETE CASCADE; +CREATE INDEX emailverification_tokens_tenant_id_index ON emailverification_tokens (app_id, tenant_id); -- EmailPassword @@ -322,6 +345,8 @@ ALTER TABLE emailpassword_pswd_reset_tokens FOREIGN KEY (app_id, user_id) REFERENCES emailpassword_users (app_id, user_id) ON DELETE CASCADE; +CREATE INDEX emailpassword_pswd_reset_tokens_user_id_index ON emailpassword_pswd_reset_tokens (app_id, user_id); + -- Passwordless ALTER TABLE passwordless_users @@ -393,6 +418,8 @@ DROP INDEX passwordless_devices_phone_number_index; CREATE INDEX passwordless_devices_phone_number_index ON passwordless_devices (app_id, tenant_id, phone_number); +CREATE INDEX passwordless_devices_tenant_id_index ON passwordless_devices (app_id, tenant_id); + ------------------------------------------------------------ ALTER TABLE passwordless_codes @@ -510,6 +537,8 @@ ALTER TABLE userid_mapping FOREIGN KEY (app_id, supertokens_user_id) REFERENCES app_id_to_user_id (app_id, user_id) ON DELETE CASCADE; +CREATE INDEX userid_mapping_supertokens_user_id_index ON userid_mapping (app_id, supertokens_user_id); + -- UserRoles ALTER TABLE roles @@ -527,6 +556,8 @@ ALTER TABLE roles FOREIGN KEY (app_id) REFERENCES apps (app_id) ON DELETE CASCADE; +CREATE INDEX roles_app_id_index ON roles (app_id); + ------------------------------------------------------------ ALTER TABLE role_permissions @@ -551,6 +582,8 @@ DROP INDEX role_permissions_permission_index; CREATE INDEX role_permissions_permission_index ON role_permissions (app_id, permission); +CREATE INDEX role_permissions_role_index ON role_permissions (app_id, role); + ------------------------------------------------------------ ALTER TABLE user_roles @@ -581,6 +614,10 @@ DROP INDEX user_roles_role_index; CREATE INDEX user_roles_role_index ON user_roles (app_id, tenant_id, role); +CREATE INDEX user_roles_tenant_id_index ON user_roles (app_id, tenant_id); + +CREATE INDEX user_roles_app_id_role_index ON user_roles (app_id, role); + -- UserMetadata ALTER TABLE user_metadata @@ -598,6 +635,8 @@ ALTER TABLE user_metadata FOREIGN KEY (app_id) REFERENCES apps (app_id) ON DELETE CASCADE; +CREATE INDEX user_metadata_app_id_index ON user_metadata (app_id); + -- Dashboard ALTER TABLE dashboard_users @@ -622,6 +661,8 @@ ALTER TABLE dashboard_users FOREIGN KEY (app_id) REFERENCES apps (app_id) ON DELETE CASCADE; +CREATE INDEX dashboard_users_app_id_index ON dashboard_users (app_id); + ------------------------------------------------------------ ALTER TABLE dashboard_user_sessions @@ -642,6 +683,8 @@ ALTER TABLE dashboard_user_sessions FOREIGN KEY (app_id, user_id) REFERENCES dashboard_users (app_id, user_id) ON DELETE CASCADE; +CREATE INDEX dashboard_user_sessions_user_id_index ON dashboard_user_sessions (app_id, user_id); + -- TOTP ALTER TABLE totp_users @@ -659,6 +702,8 @@ ALTER TABLE totp_users FOREIGN KEY (app_id) REFERENCES apps (app_id) ON DELETE CASCADE; +CREATE INDEX totp_users_app_id_index ON totp_users (app_id); + ------------------------------------------------------------ ALTER TABLE totp_user_devices @@ -679,6 +724,8 @@ ALTER TABLE totp_user_devices FOREIGN KEY (app_id, user_id) REFERENCES totp_users (app_id, user_id) ON DELETE CASCADE; +CREATE INDEX totp_user_devices_user_id_index ON totp_user_devices (app_id, user_id); + ------------------------------------------------------------ ALTER TABLE totp_used_codes @@ -709,6 +756,10 @@ DROP INDEX totp_used_codes_expiry_time_ms_index; CREATE INDEX totp_used_codes_expiry_time_ms_index ON totp_used_codes (app_id, tenant_id, expiry_time_ms); +CREATE INDEX totp_used_codes_user_id_index ON totp_used_codes (app_id, user_id); + +CREATE INDEX totp_used_codes_tenant_id_index ON totp_used_codes (app_id, tenant_id); + -- ActiveUsers ALTER TABLE user_last_active @@ -720,6 +771,13 @@ ALTER TABLE user_last_active ALTER TABLE user_last_active ADD CONSTRAINT user_last_active_pkey PRIMARY KEY (app_id, user_id); + +ALTER TABLE user_last_active + ADD CONSTRAINT user_last_active_app_id_fkey + FOREIGN KEY (app_id) + REFERENCES apps (app_id) ON DELETE CASCADE; + +CREATE INDEX user_last_active_app_id_index ON user_last_active (app_id); ``` ## [3.0.0] - 2023-04-05 diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index cd0375eb..796ba91f 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -712,6 +712,8 @@ public boolean isUserIdBeingUsedInNonAuthRecipe(AppIdentifier appIdentifier, Str } } else if (className.equals(JWTRecipeStorage.class.getName())) { return false; + } else if (className.equals(ActiveUsersStorage.class.getName())) { + return ActiveUsersQueries.getLastActiveByUserId(this, appIdentifier, userId) != null; } else { throw new IllegalStateException("ClassName: " + className + " is not part of NonAuthRecipeStorage"); } @@ -797,6 +799,12 @@ public void addInfoToNonAuthRecipesBasedOnUserId(TenantIdentifier tenantIdentifi } } else if (className.equals(JWTRecipeStorage.class.getName())) { /* Since JWT recipe tables do not store userId we do not add any data to them */ + } else if (className.equals(ActiveUsersStorage.class.getName())) { + try { + ActiveUsersQueries.updateUserLastActive(this, tenantIdentifier.toAppIdentifier(), userId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } } else { throw new IllegalStateException("ClassName: " + className + " is not part of NonAuthRecipeStorage"); } @@ -1091,6 +1099,8 @@ public void addEmailVerificationToken(TenantIdentifier tenantIdentifier, throw new TenantOrAppNotFoundException(tenantIdentifier); } } + + throw new StorageQueryException(e); } } @@ -1351,10 +1361,19 @@ public int countUsersEnabledTotpAndActiveSince(AppIdentifier appIdentifier, long } @Override - public boolean doesUserIdExist(TenantIdentifier tenantIdentifierIdentifier, String userId) + public void deleteUserActive(AppIdentifier appIdentifier, String userId) throws StorageQueryException { + try { + ActiveUsersQueries.deleteUserActive(this, appIdentifier, userId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public boolean doesUserIdExist(TenantIdentifier tenantIdentifier, String userId) throws StorageQueryException { try { - return GeneralQueries.doesUserIdExist(this, tenantIdentifierIdentifier, userId); + return GeneralQueries.doesUserIdExist(this, tenantIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1499,7 +1518,6 @@ public void deleteDevicesByEmail_Transaction(TenantIdentifier tenantIdentifier, } } - @Override public void deleteDevicesByPhoneNumber_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String phoneNumber, String userId) throws StorageQueryException { @@ -1549,9 +1567,7 @@ public void deleteCode_Transaction(TenantIdentifier tenantIdentifier, Transactio } @Override - public void updateUserEmail_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String - userId, - String email) + public void updateUserEmail_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId, String email) throws StorageQueryException, UnknownUserIdException, DuplicateEmailException { Connection sqlCon = (Connection) con.getConnection(); try { @@ -1817,9 +1833,7 @@ public io.supertokens.pluginInterface.passwordless.UserInfo getUserById(AppIdent } @Override - public io.supertokens.pluginInterface.passwordless.UserInfo getUserByEmail(TenantIdentifier - tenantIdentifier, - String email) + public io.supertokens.pluginInterface.passwordless.UserInfo getUserByEmail(TenantIdentifier tenantIdentifier, String email) throws StorageQueryException { try { return PasswordlessQueries.getUserByEmail(this, tenantIdentifier, email); @@ -1829,9 +1843,7 @@ public io.supertokens.pluginInterface.passwordless.UserInfo getUserByEmail(Tenan } @Override - public io.supertokens.pluginInterface.passwordless.UserInfo getUserByPhoneNumber(TenantIdentifier - tenantIdentifier, - String phoneNumber) + public io.supertokens.pluginInterface.passwordless.UserInfo getUserByPhoneNumber(TenantIdentifier tenantIdentifier, String phoneNumber) throws StorageQueryException { try { return PasswordlessQueries.getUserByPhoneNumber(this, tenantIdentifier, phoneNumber); @@ -1841,8 +1853,7 @@ public io.supertokens.pluginInterface.passwordless.UserInfo getUserByPhoneNumber } @Override - public JsonObject getUserMetadata(AppIdentifier appIdentifier, String userId) throws - StorageQueryException { + public JsonObject getUserMetadata(AppIdentifier appIdentifier, String userId) throws StorageQueryException { try { return UserMetadataQueries.getUserMetadata(this, appIdentifier, userId); } catch (SQLException e) { @@ -1863,9 +1874,7 @@ public JsonObject getUserMetadata_Transaction(AppIdentifier appIdentifier, Trans } @Override - public int setUserMetadata_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String - userId, - JsonObject metadata) + public int setUserMetadata_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId, JsonObject metadata) throws StorageQueryException, TenantOrAppNotFoundException { Connection sqlCon = (Connection) con.getConnection(); try { @@ -2016,9 +2025,7 @@ public void deleteAllRolesForUser(AppIdentifier appIdentifier, String userId) th } @Override - public boolean deleteRoleForUser_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection - con, - String userId, String role) + public boolean deleteRoleForUser_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, String userId, String role) throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); @@ -2076,9 +2083,7 @@ public void addPermissionToRoleOrDoNothingIfExists_Transaction(AppIdentifier app } @Override - public boolean deletePermissionForRole_Transaction(AppIdentifier appIdentifier, TransactionConnection - con, - String role, String permission) + public boolean deletePermissionForRole_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String role, String permission) throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { @@ -2090,9 +2095,7 @@ public boolean deletePermissionForRole_Transaction(AppIdentifier appIdentifier, } @Override - public int deleteAllPermissionsForRole_Transaction(AppIdentifier appIdentifier, TransactionConnection - con, - String role) + public int deleteAllPermissionsForRole_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String role) throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { @@ -2104,9 +2107,7 @@ public int deleteAllPermissionsForRole_Transaction(AppIdentifier appIdentifier, } @Override - public boolean doesRoleExist_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String - role) - throws StorageQueryException { + public boolean doesRoleExist_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String role) throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { return UserRolesQueries.doesRoleExist_transaction(this, sqlCon, appIdentifier, role); @@ -2116,9 +2117,8 @@ public boolean doesRoleExist_Transaction(AppIdentifier appIdentifier, Transactio } @Override - public void createUserIdMapping(AppIdentifier appIdentifier, String superTokensUserId, String - externalUserId, - @Nullable String externalUserIdInfo) + public void createUserIdMapping(AppIdentifier appIdentifier, String superTokensUserId, String externalUserId, + @org.jetbrains.annotations.Nullable String externalUserIdInfo) throws StorageQueryException, UnknownSuperTokensUserIdException, UserIdMappingAlreadyExistsException { try { UserIdMappingQueries.createUserIdMapping(this, appIdentifier, superTokensUserId, externalUserId, @@ -2152,9 +2152,7 @@ public void createUserIdMapping(AppIdentifier appIdentifier, String superTokensU } @Override - public boolean deleteUserIdMapping(AppIdentifier appIdentifier, String userId, - boolean isSuperTokensUserId) - throws StorageQueryException { + public boolean deleteUserIdMapping(AppIdentifier appIdentifier, String userId, boolean isSuperTokensUserId) throws StorageQueryException { try { if (isSuperTokensUserId) { return UserIdMappingQueries.deleteUserIdMappingWithSuperTokensUserId(this, appIdentifier, @@ -2168,9 +2166,7 @@ public boolean deleteUserIdMapping(AppIdentifier appIdentifier, String userId, } @Override - public UserIdMapping getUserIdMapping(AppIdentifier appIdentifier, String userId, - boolean isSuperTokensUserId) - throws StorageQueryException { + public UserIdMapping getUserIdMapping(AppIdentifier appIdentifier, String userId, boolean isSuperTokensUserId) throws StorageQueryException { try { if (isSuperTokensUserId) { return UserIdMappingQueries.getuseraIdMappingWithSuperTokensUserId(this, appIdentifier, @@ -2414,9 +2410,6 @@ public boolean removeUserIdFromTenant(TenantIdentifier tenantIdentifier, String }); } catch (StorageTransactionLogicException e) { if (e.actualException instanceof SQLException) { - PostgreSQLConfig config = Config.getConfig(this); - ServerErrorMessage serverErrorMessage = ((PSQLException) e.actualException).getServerErrorMessage(); - throw new StorageQueryException(e.actualException); } else if (e.actualException instanceof StorageQueryException) { throw (StorageQueryException) e.actualException; @@ -2426,8 +2419,7 @@ public boolean removeUserIdFromTenant(TenantIdentifier tenantIdentifier, String } @Override - public boolean deleteDashboardUserWithUserId(AppIdentifier appIdentifier, String userId) throws - StorageQueryException { + public boolean deleteDashboardUserWithUserId(AppIdentifier appIdentifier, String userId) throws StorageQueryException { try { return DashboardQueries.deleteDashboardUserWithUserId(this, appIdentifier, userId); } catch (SQLException e) { @@ -2477,8 +2469,7 @@ public DashboardSessionInfo getSessionInfoWithSessionId(AppIdentifier appIdentif } @Override - public boolean revokeSessionWithSessionId(AppIdentifier appIdentifier, String sessionId) throws - StorageQueryException { + public boolean revokeSessionWithSessionId(AppIdentifier appIdentifier, String sessionId) throws StorageQueryException { try { return DashboardQueries.deleteDashboardUserSessionWithSessionId(this, appIdentifier, sessionId); @@ -2488,12 +2479,9 @@ public boolean revokeSessionWithSessionId(AppIdentifier appIdentifier, String se } @Override - public void updateDashboardUsersEmailWithUserId_Transaction(AppIdentifier - appIdentifier, TransactionConnection - con, String userId, + public void updateDashboardUsersEmailWithUserId_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId, String newEmail) throws StorageQueryException, - io.supertokens.pluginInterface.dashboard.exceptions.DuplicateEmailException, - UserIdNotFoundException { + io.supertokens.pluginInterface.dashboard.exceptions.DuplicateEmailException, UserIdNotFoundException { Connection sqlCon = (Connection) con.getConnection(); try { if (!DashboardQueries.updateDashboardUsersEmailWithUserId_Transaction(this, @@ -2689,6 +2677,7 @@ public void updateDeviceName(AppIdentifier appIdentifier, String userId, String throw new DeviceAlreadyExistsException(); } } + throw new StorageQueryException(e); } } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java index b0877928..52508166 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java @@ -1,21 +1,35 @@ package io.supertokens.storage.postgresql.queries; +import java.math.BigInteger; import java.sql.SQLException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.storage.postgresql.Start; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.storage.postgresql.config.Config; +import io.supertokens.storage.postgresql.utils.Utils; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.update; public class ActiveUsersQueries { static String getQueryToCreateUserLastActiveTable(Start start) { + String schema = Config.getConfig(start).getTableSchema(); + return "CREATE TABLE IF NOT EXISTS " + Config.getConfig(start).getUserLastActiveTable() + " (" + "app_id VARCHAR(64) DEFAULT 'public'," + "user_id VARCHAR(128)," - + "last_active_time BIGINT," + "PRIMARY KEY(app_id, user_id)" + " );"; + + "last_active_time BIGINT," + + "PRIMARY KEY(app_id, user_id)," + + "CONSTRAINT " + Utils.getConstraintName(schema, Config.getConfig(start).getUserLastActiveTable(), "app_id", "fkey") + + " FOREIGN KEY(app_id)" + + " REFERENCES " + Config.getConfig(start).getAppsTable() + " (app_id) ON DELETE CASCADE" + + ");"; + } + + static String getQueryToCreateAppIdIndexForUserLastActiveTable(Start start) { + return "CREATE INDEX IF NOT EXISTS user_last_active_app_id_index ON " + + Config.getConfig(start).getUserLastActiveTable() + "(app_id);"; } public static int countUsersActiveSince(Start start, AppIdentifier appIdentifier, long sinceTime) throws SQLException, StorageQueryException { @@ -33,7 +47,6 @@ public static int countUsersActiveSince(Start start, AppIdentifier appIdentifier }); } - 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 = ?"; @@ -77,4 +90,35 @@ public static int updateUserLastActive(Start start, AppIdentifier appIdentifier, pst.setLong(4, now); }); } + + public static Long getLastActiveByUserId(Start start, AppIdentifier appIdentifier, String userId) + throws StorageQueryException { + String QUERY = "SELECT last_active_time FROM " + Config.getConfig(start).getUserLastActiveTable() + + " WHERE app_id = ? AND user_id = ?"; + + try { + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }, res -> { + if (res.next()) { + return res.getLong("last_active_time"); + } + return null; + }); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + public static void deleteUserActive(Start start, AppIdentifier appIdentifier, String userId) + throws StorageQueryException, SQLException { + String QUERY = "DELETE FROM " + Config.getConfig(start).getUserLastActiveTable() + + " WHERE app_id = ? AND user_id = ?"; + + update(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); + } } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/DashboardQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/DashboardQueries.java index 744e27a5..135d2f7a 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/DashboardQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/DashboardQueries.java @@ -56,6 +56,11 @@ public static String getQueryToCreateDashboardUsersTable(Start start) { // @formatter:on } + public static String getQueryToCreateAppIdIndexForDashboardUsersTable(Start start) { + return "CREATE INDEX dashboard_users_app_id_index ON " + + Config.getConfig(start).getDashboardUsersTable() + "(app_id);"; + } + public static String getQueryToCreateDashboardUserSessionsTable(Start start) { String schema = Config.getConfig(start).getTableSchema(); String tableName = Config.getConfig(start).getDashboardSessionsTable(); @@ -80,6 +85,11 @@ static String getQueryToCreateDashboardUserSessionsExpiryIndex(Start start) { + Config.getConfig(start).getDashboardSessionsTable() + "(expiry);"; } + public static String getQueryToCreateUserIdIndexForDashboardUserSessionsTable(Start start) { + return "CREATE INDEX dashboard_user_sessions_user_id_index ON " + + Config.getConfig(start).getDashboardSessionsTable() + "(app_id, user_id);"; + } + public static void createDashboardUser(Start start, AppIdentifier appIdentifier, String userId, String email, String passwordHash, long timeJoined) throws SQLException, StorageQueryException { @@ -107,7 +117,7 @@ public static boolean deleteDashboardUserWithUserId(Start start, AppIdentifier a return rowUpdatedCount > 0; - }; + } public static DashboardUser[] getAllDashBoardUsers(Start start, AppIdentifier appIdentifier) throws SQLException, StorageQueryException { String QUERY = "SELECT * FROM " diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java index 9b28c672..81d869d6 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java @@ -48,7 +48,8 @@ static String getQueryToCreateUsersTable(Start start) { + "app_id VARCHAR(64) DEFAULT 'public'," + "user_id CHAR(36) NOT NULL," + "email VARCHAR(256) NOT NULL," - + "password_hash VARCHAR(256) NOT NULL," + "time_joined BIGINT NOT NULL," + + "password_hash VARCHAR(256) NOT NULL," + + "time_joined BIGINT NOT NULL," + "CONSTRAINT " + Utils.getConstraintName(schema, emailPasswordUsersTable, "user_id", "fkey") + " FOREIGN KEY(app_id, user_id)" + " REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable() + " (app_id, user_id) ON DELETE CASCADE," @@ -98,6 +99,11 @@ static String getQueryToCreatePasswordResetTokensTable(Start start) { // @formatter:on } + public static String getQueryToCreateUserIdIndexForPasswordResetTokensTable(Start start) { + return "CREATE INDEX emailpassword_pswd_reset_tokens_user_id_index ON " + + Config.getConfig(start).getPasswordResetTokensTable() + "(app_id, user_id);"; + } + static String getQueryToCreatePasswordResetTokenExpiryIndex(Start start) { return "CREATE INDEX emailpassword_password_reset_token_expiry_index ON " + Config.getConfig(start).getPasswordResetTokensTable() + "(token_expiry);"; diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/EmailVerificationQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/EmailVerificationQueries.java index b24bb04e..afe360fb 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/EmailVerificationQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/EmailVerificationQueries.java @@ -57,6 +57,11 @@ static String getQueryToCreateEmailVerificationTable(Start start) { // @formatter:on } + public static String getQueryToCreateAppIdIndexForEmailVerificationTable(Start start) { + return "CREATE INDEX emailverification_verified_emails_app_id_index ON " + + Config.getConfig(start).getEmailVerificationTable() + "(app_id);"; + } + static String getQueryToCreateEmailVerificationTokensTable(Start start) { String schema = Config.getConfig(start).getTableSchema(); String emailVerificationTokensTable = Config.getConfig(start).getEmailVerificationTokensTable(); @@ -77,6 +82,11 @@ static String getQueryToCreateEmailVerificationTokensTable(Start start) { // @formatter:on } + public static String getQueryToCreateTenantIdIndexForEmailVerificationTokensTable(Start start) { + return "CREATE INDEX emailverification_tokens_tenant_id_index ON " + + Config.getConfig(start).getEmailVerificationTokensTable() + "(app_id, tenant_id);"; + } + static String getQueryToCreateEmailVerificationTokenExpiryIndex(Start start) { return "CREATE INDEX emailverification_tokens_index ON " + Config.getConfig(start).getEmailVerificationTokensTable() + "(token_expiry);"; diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java index 75ffe291..23ad592a 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java @@ -43,12 +43,13 @@ import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.update; import static io.supertokens.storage.postgresql.config.Config.getConfig; -import static io.supertokens.storage.postgresql.queries.EmailPasswordQueries.getQueryToCreatePasswordResetTokenExpiryIndex; -import static io.supertokens.storage.postgresql.queries.EmailPasswordQueries.getQueryToCreatePasswordResetTokensTable; +import static io.supertokens.storage.postgresql.queries.EmailPasswordQueries.*; import static io.supertokens.storage.postgresql.queries.EmailVerificationQueries.*; +import static io.supertokens.storage.postgresql.queries.JWTSigningQueries.getQueryToCreateAppIdIndexForJWTSigningTable; import static io.supertokens.storage.postgresql.queries.JWTSigningQueries.getQueryToCreateJWTSigningTable; import static io.supertokens.storage.postgresql.queries.PasswordlessQueries.*; import static io.supertokens.storage.postgresql.queries.SessionQueries.*; +import static io.supertokens.storage.postgresql.queries.UserMetadataQueries.getQueryToCreateAppIdIndexForUserMetadataTable; import static io.supertokens.storage.postgresql.queries.UserMetadataQueries.getQueryToCreateUserMetadataTable; public class GeneralQueries { @@ -86,6 +87,16 @@ static String getQueryToCreateUsersTable(Start start) { // @formatter:on } + public static String getQueryToCreateUserIdIndexForUsersTable(Start start) { + return "CREATE INDEX IF NOT EXISTS all_auth_recipe_user_id_index ON " + + Config.getConfig(start).getUsersTable() + "(app_id, user_id);"; + } + + public static String getQueryToCreateTenantIdIndexForUsersTable(Start start) { + return "CREATE INDEX IF NOT EXISTS all_auth_recipe_tenant_id_index ON " + + Config.getConfig(start).getUsersTable() + "(app_id, tenant_id);"; + } + static String getQueryToCreateUserPaginationIndex(Start start) { return "CREATE INDEX all_auth_recipe_users_pagination_index ON " + Config.getConfig(start).getUsersTable() + "(time_joined DESC, user_id DESC, tenant_id DESC, app_id DESC);"; @@ -98,7 +109,8 @@ private static String getQueryToCreateAppsTable(Start start) { return "CREATE TABLE IF NOT EXISTS " + appsTable + " (" + "app_id VARCHAR(64) NOT NULL DEFAULT 'public'," + "created_at_time BIGINT ," - + "CONSTRAINT " + Utils.getConstraintName(schema, appsTable, null, "pkey") + " PRIMARY KEY(app_id)" + + + "CONSTRAINT " + Utils.getConstraintName(schema, appsTable, null, "pkey") + + " PRIMARY KEY(app_id)" + " );"; // @formatter:on } @@ -120,6 +132,11 @@ private static String getQueryToCreateTenantsTable(Start start) { // @formatter:on } + static String getQueryToCreateAppIdIndexForTenantsTable(Start start) { + return "CREATE INDEX IF NOT EXISTS tenants_app_id_index ON " + + Config.getConfig(start).getTenantsTable() + "(app_id);"; + } + private static String getQueryToCreateKeyValueTable(Start start) { String schema = Config.getConfig(start).getTableSchema(); String keyValueTable = Config.getConfig(start).getKeyValueTable(); @@ -139,6 +156,11 @@ private static String getQueryToCreateKeyValueTable(Start start) { // @formatter:on } + static String getQueryToCreateTenantIdIndexForKeyValueTable(Start start) { + return "CREATE INDEX IF NOT EXISTS key_value_tenant_id_index ON " + + Config.getConfig(start).getKeyValueTable() + "(app_id, tenant_id);"; + } + private static String getQueryToCreateAppIdToUserIdTable(Start start) { String schema = Config.getConfig(start).getTableSchema(); String appToUserTable = Config.getConfig(start).getAppIdToUserIdTable(); @@ -156,6 +178,11 @@ private static String getQueryToCreateAppIdToUserIdTable(Start start) { // @formatter:on } + static String getQueryToCreateAppIdIndexForAppIdToUserIdTable(Start start) { + return "CREATE INDEX IF NOT EXISTS app_id_to_user_id_app_id_index ON " + + Config.getConfig(start).getAppIdToUserIdTable() + "(app_id);"; + } + public static void createTablesIfNotExists(Start start) throws SQLException, StorageQueryException { int numberOfRetries = 0; boolean retry = true; @@ -170,16 +197,25 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto if (!doesTableExists(start, Config.getConfig(start).getTenantsTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); update(start, getQueryToCreateTenantsTable(start), NO_OP_SETTER); + + // index + update(start, getQueryToCreateAppIdIndexForTenantsTable(start), NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getKeyValueTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); update(start, getQueryToCreateKeyValueTable(start), NO_OP_SETTER); + + // index + update(start, getQueryToCreateTenantIdIndexForKeyValueTable(start), NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getAppIdToUserIdTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); update(start, getQueryToCreateAppIdToUserIdTable(start), NO_OP_SETTER); + + // index + update(start, getQueryToCreateAppIdIndexForAppIdToUserIdTable(start), NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getUsersTable())) { @@ -193,11 +229,17 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto if (!doesTableExists(start, Config.getConfig(start).getUserLastActiveTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); update(start, ActiveUsersQueries.getQueryToCreateUserLastActiveTable(start), NO_OP_SETTER); + + // Index + update(start, ActiveUsersQueries.getQueryToCreateAppIdIndexForUserLastActiveTable(start), NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getAccessTokenSigningKeysTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); update(start, getQueryToCreateAccessTokenSigningKeysTable(start), NO_OP_SETTER); + + // index + update(start, getQueryToCreateAppIdIndexForAccessTokenSigningKeysTable(start), NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getSessionInfoTable())) { @@ -206,6 +248,7 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto // index update(start, getQueryToCreateSessionExpiryIndex(start), NO_OP_SETTER); + update(start, getQueryToCreateTenantIdIndexForSessionInfoTable(start), NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getTenantConfigsTable())) { @@ -217,12 +260,20 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto getInstance(start).addState(CREATING_NEW_TABLE, null); update(start, MultitenancyQueries.getQueryToCreateTenantThirdPartyProvidersTable(start), NO_OP_SETTER); + + // index + update(start, MultitenancyQueries.getQueryToCreateTenantIdIndexForTenantThirdPartyProvidersTable(start), + NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getTenantThirdPartyProviderClientsTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); update(start, MultitenancyQueries.getQueryToCreateTenantThirdPartyProviderClientsTable(start), NO_OP_SETTER); + + // index + update(start, MultitenancyQueries.getQueryToCreateThirdPartyIdIndexForTenantThirdPartyProviderClientsTable(start), + NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getEmailPasswordUsersTable())) { @@ -241,11 +292,15 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto update(start, getQueryToCreatePasswordResetTokensTable(start), NO_OP_SETTER); // index update(start, getQueryToCreatePasswordResetTokenExpiryIndex(start), NO_OP_SETTER); + update(start, getQueryToCreateUserIdIndexForPasswordResetTokensTable(start), NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getEmailVerificationTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); update(start, getQueryToCreateEmailVerificationTable(start), NO_OP_SETTER); + + // index + update(start, getQueryToCreateAppIdIndexForEmailVerificationTable(start), NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getEmailVerificationTokensTable())) { @@ -253,6 +308,7 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto update(start, getQueryToCreateEmailVerificationTokensTable(start), NO_OP_SETTER); // index update(start, getQueryToCreateEmailVerificationTokenExpiryIndex(start), NO_OP_SETTER); + update(start, getQueryToCreateTenantIdIndexForEmailVerificationTokensTable(start), NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getThirdPartyUsersTable())) { @@ -271,11 +327,18 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto if (!doesTableExists(start, Config.getConfig(start).getJWTSigningKeysTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); update(start, getQueryToCreateJWTSigningTable(start), NO_OP_SETTER); + + // index + update(start, getQueryToCreateAppIdIndexForJWTSigningTable(start), NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getPasswordlessUsersTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); update(start, PasswordlessQueries.getQueryToCreateUsersTable(start), NO_OP_SETTER); + + // index + update(start, getQueryToCreateUserIdIndexForUsersTable(start), NO_OP_SETTER); + update(start, getQueryToCreateTenantIdIndexForUsersTable(start), NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getPasswordlessUserToTenantTable())) { @@ -290,6 +353,7 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto // index update(start, getQueryToCreateDeviceEmailIndex(start), NO_OP_SETTER); update(start, getQueryToCreateDevicePhoneNumberIndex(start), NO_OP_SETTER); + update(start, getQueryToCreateTenantIdIndexForDevicesTable(start), NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getPasswordlessCodesTable())) { @@ -297,8 +361,8 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto update(start, getQueryToCreateCodesTable(start), NO_OP_SETTER); // index update(start, getQueryToCreateCodeCreatedAtIndex(start), NO_OP_SETTER); - } + } // This PostgreSQL specific, because it's created automatically in MySQL and it // doesn't support "create // index if not exists" @@ -309,11 +373,17 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto if (!doesTableExists(start, Config.getConfig(start).getUserMetadataTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); update(start, getQueryToCreateUserMetadataTable(start), NO_OP_SETTER); + + // Index + update(start, getQueryToCreateAppIdIndexForUserMetadataTable(start), NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getRolesTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); update(start, UserRolesQueries.getQueryToCreateRolesTable(start), NO_OP_SETTER); + + // Index + update(start, UserRolesQueries.getQueryToCreateAppIdIndexForRolesTable(start), NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getUserRolesPermissionsTable())) { @@ -321,23 +391,33 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto update(start, UserRolesQueries.getQueryToCreateRolePermissionsTable(start), NO_OP_SETTER); // index update(start, UserRolesQueries.getQueryToCreateRolePermissionsPermissionIndex(start), NO_OP_SETTER); + update(start, UserRolesQueries.getQueryToCreateRoleIndexForRolePermissionsTable(start), NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getUserRolesTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); update(start, UserRolesQueries.getQueryToCreateUserRolesTable(start), NO_OP_SETTER); + // index update(start, UserRolesQueries.getQueryToCreateUserRolesRoleIndex(start), NO_OP_SETTER); + update(start, UserRolesQueries.getQueryToCreateTenantIdIndexForUserRolesTable(start), NO_OP_SETTER); + update(start, UserRolesQueries.getQueryToCreateRoleIndexForUserRolesTable(start), NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getUserIdMappingTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); update(start, UserIdMappingQueries.getQueryToCreateUserIdMappingTable(start), NO_OP_SETTER); + + // index + update(start, UserIdMappingQueries.getQueryToCreateSupertokensUserIdIndexForUserIdMappingTable(start), NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getDashboardUsersTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); update(start, DashboardQueries.getQueryToCreateDashboardUsersTable(start), NO_OP_SETTER); + + // Index + update(start, DashboardQueries.getQueryToCreateAppIdIndexForDashboardUsersTable(start), NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getDashboardSessionsTable())) { @@ -346,16 +426,24 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto // index update(start, DashboardQueries.getQueryToCreateDashboardUserSessionsExpiryIndex(start), NO_OP_SETTER); + update(start, DashboardQueries.getQueryToCreateUserIdIndexForDashboardUserSessionsTable(start), + NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getTotpUsersTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); update(start, TOTPQueries.getQueryToCreateUsersTable(start), NO_OP_SETTER); + + // index + update(start, TOTPQueries.getQueryToCreateAppIdIndexForUsersTable(start), NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getTotpUserDevicesTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); update(start, TOTPQueries.getQueryToCreateUserDevicesTable(start), NO_OP_SETTER); + + // index + update(start, TOTPQueries.getQueryToCreateUserIdIndexForUserDevicesTable(start), NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getTotpUsedCodesTable())) { @@ -363,6 +451,8 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto update(start, TOTPQueries.getQueryToCreateUsedCodesTable(start), NO_OP_SETTER); // index: update(start, TOTPQueries.getQueryToCreateUsedCodesExpiryTimeIndex(start), NO_OP_SETTER); + update(start, TOTPQueries.getQueryToCreateUserIdIndexForUsedCodesTable(start), NO_OP_SETTER); + update(start, TOTPQueries.getQueryToCreateTenantIdIndexForUsedCodesTable(start), NO_OP_SETTER); } } catch (Exception e) { diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/JWTSigningQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/JWTSigningQueries.java index a9cf7080..f57c4402 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/JWTSigningQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/JWTSigningQueries.java @@ -64,6 +64,11 @@ static String getQueryToCreateJWTSigningTable(Start start) { // @formatter:on } + public static String getQueryToCreateAppIdIndexForJWTSigningTable(Start start) { + return "CREATE INDEX IF NOT EXISTS jwt_signing_keys_app_id_index ON " + + getConfig(start).getJWTSigningKeysTable() + " (app_id);"; + } + public static List getJWTSigningKeys_Transaction(Start start, Connection con, AppIdentifier appIdentifier) throws SQLException, StorageQueryException { diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/MultitenancyQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/MultitenancyQueries.java index 3db38834..592cdbd0 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/MultitenancyQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/MultitenancyQueries.java @@ -88,6 +88,11 @@ static String getQueryToCreateTenantThirdPartyProvidersTable(Start start) { // @formatter:on } + public static String getQueryToCreateTenantIdIndexForTenantThirdPartyProvidersTable(Start start) { + return "CREATE INDEX IF NOT EXISTS tenant_thirdparty_providers_tenant_id_index ON " + + getConfig(start).getTenantThirdPartyProvidersTable() + " (connection_uri_domain, app_id, tenant_id);"; + } + static String getQueryToCreateTenantThirdPartyProviderClientsTable(Start start) { String schema = Config.getConfig(start).getTableSchema(); String tenantThirdPartyProvidersTable = Config.getConfig(start).getTenantThirdPartyProviderClientsTable(); @@ -109,6 +114,11 @@ static String getQueryToCreateTenantThirdPartyProviderClientsTable(Start start) + ");"; } + public static String getQueryToCreateThirdPartyIdIndexForTenantThirdPartyProviderClientsTable(Start start) { + return "CREATE INDEX IF NOT EXISTS tenant_thirdparty_provider_clients_third_party_id_index ON " + + getConfig(start).getTenantThirdPartyProviderClientsTable() + " (connection_uri_domain, app_id, tenant_id, third_party_id);"; + } + private static void executeCreateTenantQueries(Start start, Connection sqlCon, TenantConfig tenantConfig) throws SQLException, StorageQueryException { diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java index 4e3efa77..0d03bc81 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java @@ -104,6 +104,11 @@ public static String getQueryToCreateDevicesTable(Start start) { + ");"; } + public static String getQueryToCreateTenantIdIndexForDevicesTable(Start start) { + return "CREATE INDEX passwordless_devices_tenant_id_index ON " + + Config.getConfig(start).getPasswordlessDevicesTable() + "(app_id, tenant_id);"; + } + public static String getQueryToCreateCodesTable(Start start) { String schema = Config.getConfig(start).getTableSchema(); String codesTable = Config.getConfig(start).getPasswordlessCodesTable(); diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/SessionQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/SessionQueries.java index a311f7a3..928fbd66 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/SessionQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/SessionQueries.java @@ -64,7 +64,11 @@ public static String getQueryToCreateSessionInfoTable(Start start) { + " REFERENCES " + Config.getConfig(start).getTenantsTable() + "(app_id, tenant_id) ON DELETE CASCADE" + ");"; // @formatter:on + } + public static String getQueryToCreateTenantIdIndexForSessionInfoTable(Start start) { + return "CREATE INDEX session_info_tenant_id_index ON " + + Config.getConfig(start).getSessionInfoTable() + "(app_id, tenant_id);"; } static String getQueryToCreateAccessTokenSigningKeysTable(Start start) { @@ -84,6 +88,11 @@ static String getQueryToCreateAccessTokenSigningKeysTable(Start start) { // @formatter:on } + public static String getQueryToCreateAppIdIndexForAccessTokenSigningKeysTable(Start start) { + return "CREATE INDEX access_token_signing_keys_app_id_index ON " + + Config.getConfig(start).getAccessTokenSigningKeysTable() + "(app_id);"; + } + static String getQueryToCreateSessionExpiryIndex(Start start) { return "CREATE INDEX session_expiry_index ON " + Config.getConfig(start).getSessionInfoTable() + "(expires_at);"; diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/TOTPQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/TOTPQueries.java index 50234cd5..dad5e52d 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/TOTPQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/TOTPQueries.java @@ -37,6 +37,11 @@ public static String getQueryToCreateUsersTable(Start start) { // @formatter:on } + public static String getQueryToCreateAppIdIndexForUsersTable(Start start) { + return "CREATE INDEX totp_users_app_id_index ON " + + Config.getConfig(start).getTotpUsersTable() + "(app_id);"; + } + public static String getQueryToCreateUserDevicesTable(Start start) { String schema = Config.getConfig(start).getTableSchema(); String tableName = Config.getConfig(start).getTotpUserDevicesTable(); @@ -58,6 +63,11 @@ public static String getQueryToCreateUserDevicesTable(Start start) { // @formatter:on } + public static String getQueryToCreateUserIdIndexForUserDevicesTable(Start start) { + return "CREATE INDEX totp_user_devices_user_id_index ON " + + Config.getConfig(start).getTotpUserDevicesTable() + "(app_id, user_id);"; + } + public static String getQueryToCreateUsedCodesTable(Start start) { String schema = Config.getConfig(start).getTableSchema(); String tableName = Config.getConfig(start).getTotpUsedCodesTable(); @@ -81,6 +91,16 @@ public static String getQueryToCreateUsedCodesTable(Start start) { // @formatter:on } + public static String getQueryToCreateUserIdIndexForUsedCodesTable(Start start) { + return "CREATE INDEX IF NOT EXISTS totp_used_codes_user_id_index ON " + + Config.getConfig(start).getTotpUsedCodesTable() + " (app_id, user_id)"; + } + + public static String getQueryToCreateTenantIdIndexForUsedCodesTable(Start start) { + return "CREATE INDEX IF NOT EXISTS totp_used_codes_tenant_id_index ON " + + Config.getConfig(start).getTotpUsedCodesTable() + " (app_id, tenant_id)"; + } + public static String getQueryToCreateUsedCodesExpiryTimeIndex(Start start) { return "CREATE INDEX IF NOT EXISTS totp_used_codes_expiry_time_ms_index ON " + Config.getConfig(start).getTotpUsedCodesTable() + " (app_id, tenant_id, expiry_time_ms)"; diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/UserIdMappingQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/UserIdMappingQueries.java index 5a16b8cb..cc600818 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/UserIdMappingQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/UserIdMappingQueries.java @@ -32,6 +32,7 @@ import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.update; +import static io.supertokens.storage.postgresql.config.Config.getConfig; public class UserIdMappingQueries { @@ -57,6 +58,11 @@ public static String getQueryToCreateUserIdMappingTable(Start start) { // @formatter:on } + public static String getQueryToCreateSupertokensUserIdIndexForUserIdMappingTable(Start start) { + return "CREATE INDEX userid_mapping_supertokens_user_id_index ON " + + getConfig(start).getUserIdMappingTable() + "(app_id, supertokens_user_id);"; + } + public static void createUserIdMapping(Start start, AppIdentifier appIdentifier, String superTokensUserId, String externalUserId, String externalUserIdInfo) throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + Config.getConfig(start).getUserIdMappingTable() diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/UserMetadataQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/UserMetadataQueries.java index f4c5d161..d645bad1 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/UserMetadataQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/UserMetadataQueries.java @@ -49,7 +49,11 @@ public static String getQueryToCreateUserMetadataTable(Start start) { + " REFERENCES " + Config.getConfig(start).getAppsTable() + " (app_id) ON DELETE CASCADE" + ");"; // @formatter:on + } + public static String getQueryToCreateAppIdIndexForUserMetadataTable(Start start) { + return "CREATE INDEX user_metadata_app_id_index ON " + + Config.getConfig(start).getUserMetadataTable() + "(app_id);"; } public static int deleteUserMetadata(Start start, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/UserRolesQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/UserRolesQueries.java index 928b5e92..3069faa6 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/UserRolesQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/UserRolesQueries.java @@ -47,10 +47,13 @@ public static String getQueryToCreateRolesTable(Start start) { + " FOREIGN KEY(app_id)" + " REFERENCES " + Config.getConfig(start).getAppsTable() + " (app_id) ON DELETE CASCADE" + ");"; - // @formatter:on } + public static String getQueryToCreateAppIdIndexForRolesTable(Start start) { + return "CREATE INDEX roles_app_id_index ON " + getConfig(start).getRolesTable() + "(app_id);"; + } + public static String getQueryToCreateRolePermissionsTable(Start start) { String tableName = getConfig(start).getUserRolesPermissionsTable(); String schema = Config.getConfig(start).getTableSchema(); @@ -68,6 +71,11 @@ public static String getQueryToCreateRolePermissionsTable(Start start) { // @formatter:on } + public static String getQueryToCreateRoleIndexForRolePermissionsTable(Start start) { + return "CREATE INDEX role_permissions_role_index ON " + getConfig(start).getUserRolesPermissionsTable() + + "(app_id, role);"; + } + static String getQueryToCreateRolePermissionsPermissionIndex(Start start) { return "CREATE INDEX role_permissions_permission_index ON " + getConfig(start).getUserRolesPermissionsTable() + "(app_id, permission);"; @@ -94,8 +102,16 @@ public static String getQueryToCreateUserRolesTable(Start start) { // @formatter:on } + public static String getQueryToCreateTenantIdIndexForUserRolesTable(Start start) { + return "CREATE INDEX IF NOT EXISTS user_roles_tenant_id_index ON " + getConfig(start).getUserRolesTable() + "(app_id, tenant_id);"; + } + + public static String getQueryToCreateRoleIndexForUserRolesTable(Start start) { + return "CREATE INDEX IF NOT EXISTS user_roles_app_id_role_index ON " + getConfig(start).getUserRolesTable() + "(app_id, role);"; + } + public static String getQueryToCreateUserRolesRoleIndex(Start start) { - return "CREATE INDEX user_roles_role_index ON " + getConfig(start).getUserRolesTable() + return "CREATE INDEX IF NOT EXISTS user_roles_role_index ON " + getConfig(start).getUserRolesTable() + "(app_id, tenant_id, role);"; } From 4d6a3356eda75f09420735464b7f125dd7b9df8e Mon Sep 17 00:00:00 2001 From: KShivendu Date: Tue, 27 Jun 2023 16:47:31 +0530 Subject: [PATCH 087/106] fix: Revert irrelevant changes --- src/main/java/io/supertokens/storage/postgresql/Start.java | 1 + .../storage/postgresql/config/PostgreSQLConfig.java | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index b4a9e788..2d91c22b 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -1392,6 +1392,7 @@ public int countUsersEnabledTotpAndActiveSince(AppIdentifier appIdentifier, long } } + @Override public int countUsersEnabledMfa(AppIdentifier appIdentifier) throws StorageQueryException { try { return ActiveUsersQueries.countUsersEnabledMfa(this, appIdentifier); diff --git a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java index 15930650..2a7bd5db 100644 --- a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java +++ b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java @@ -127,7 +127,7 @@ public static Set getValidFields() { } public String getTableSchema() { - return postgresql_table_schema.trim(); + return postgresql_table_schema; } public int getConnectionPoolSize() { @@ -211,8 +211,7 @@ public String getSessionInfoTable() { } public String getEmailPasswordUserToTenantTable() { - String tableName = "emailpassword_user_to_tenant"; - return addSchemaAndPrefixToTableName(tableName); + return addSchemaAndPrefixToTableName("emailpassword_user_to_tenant"); } public String getEmailPasswordUsersTable() { From 4cf61bbff44b1e13975d1e894fecba0afbca6474 Mon Sep 17 00:00:00 2001 From: Kumar Shivendu Date: Thu, 28 Sep 2023 16:47:56 +0530 Subject: [PATCH 088/106] refactor: Replace TotpNotEnabledError with UnknownUserIdTotpError (#133) * refactor: Replace totp not enabled error with unknown device error * Replace TotpNotEnabledError with UnknownUserIdTotpError * chores: Update CHANGELOG * fix: build * fix: totp queries --------- Co-authored-by: Sattvik Chakravarthy --- CHANGELOG.md | 1 + .../supertokens/storage/postgresql/Start.java | 74 +++++++++++++------ .../postgresql/queries/TOTPQueries.java | 31 ++++---- .../storage/postgresql/test/DeadlockTest.java | 10 +-- .../postgresql/test/StorageLayerTest.java | 6 +- 5 files changed, 80 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9788ba2d..7666632c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +- Replace `TotpNotEnabledError` with `UnknownUserIdTotpError`. - Support for MFA recipe ## [5.0.0] - 2023-09-19 diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 9b4a1a9b..ae423548 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -49,10 +49,7 @@ import io.supertokens.pluginInterface.jwt.exceptions.DuplicateKeyIdException; import io.supertokens.pluginInterface.jwt.sqlstorage.JWTRecipeSQLStorage; import io.supertokens.pluginInterface.mfa.MfaStorage; -import io.supertokens.pluginInterface.multitenancy.AppIdentifier; -import io.supertokens.pluginInterface.multitenancy.MultitenancyStorage; -import io.supertokens.pluginInterface.multitenancy.TenantConfig; -import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.multitenancy.*; import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateClientTypeException; import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateTenantException; import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateThirdPartyIdException; @@ -72,8 +69,8 @@ import io.supertokens.pluginInterface.totp.TOTPStorage; import io.supertokens.pluginInterface.totp.TOTPUsedCode; import io.supertokens.pluginInterface.totp.exception.DeviceAlreadyExistsException; -import io.supertokens.pluginInterface.totp.exception.TotpNotEnabledException; import io.supertokens.pluginInterface.totp.exception.UnknownDeviceException; +import io.supertokens.pluginInterface.totp.exception.UnknownTotpUserIdException; import io.supertokens.pluginInterface.totp.exception.UsedCodeAlreadyExistsException; import io.supertokens.pluginInterface.totp.sqlStorage.TOTPSQLStorage; import io.supertokens.pluginInterface.useridmapping.UserIdMapping; @@ -833,12 +830,13 @@ public void addInfoToNonAuthRecipesBasedOnUserId(TenantIdentifier tenantIdentifi } else if (className.equals(TOTPStorage.class.getName())) { try { TOTPDevice device = new TOTPDevice(userId, "testDevice", "secret", 0, 30, false); - TOTPQueries.createDevice(this, tenantIdentifier.toAppIdentifier(), device); this.startTransaction(con -> { try { long now = System.currentTimeMillis(); + Connection sqlCon = (Connection) con.getConnection(); + TOTPQueries.createDevice_Transaction(this, sqlCon, tenantIdentifier.toAppIdentifier(), device); TOTPQueries.insertUsedCode_Transaction(this, - (Connection) con.getConnection(), tenantIdentifier, + sqlCon, tenantIdentifier, new TOTPUsedCode(userId, "123456", true, 1000 + now, now)); } catch (SQLException e) { throw new StorageTransactionLogicException(e); @@ -1339,7 +1337,7 @@ public int countUsersEnabledTotpAndActiveSince(AppIdentifier appIdentifier, long throw new StorageQueryException(e); } } - + @Override public int countUsersEnabledMfa(AppIdentifier appIdentifier) throws StorageQueryException { try { @@ -2633,26 +2631,60 @@ public void revokeExpiredSessions() throws StorageQueryException { } // TOTP recipe: + @TestOnly @Override public void createDevice(AppIdentifier appIdentifier, TOTPDevice device) - throws StorageQueryException, DeviceAlreadyExistsException, TenantOrAppNotFoundException { + throws DeviceAlreadyExistsException, TenantOrAppNotFoundException, StorageQueryException { try { - TOTPQueries.createDevice(this, appIdentifier, device); + startTransaction(con -> { + try { + createDevice_Transaction(con, new AppIdentifier(null, null), device); + } catch (DeviceAlreadyExistsException | TenantOrAppNotFoundException e) { + throw new StorageTransactionLogicException(e); + } + return null; + }); } catch (StorageTransactionLogicException e) { - Exception actualException = e.actualException; + if (e.actualException instanceof DeviceAlreadyExistsException) { + throw (DeviceAlreadyExistsException) e.actualException; + } else if (e.actualException instanceof TenantOrAppNotFoundException) { + throw (TenantOrAppNotFoundException) e.actualException; + } else if (e.actualException instanceof StorageQueryException) { + throw (StorageQueryException) e.actualException; + } + } + } + + @Override + public TOTPDevice createDevice_Transaction(TransactionConnection con, AppIdentifier appIdentifier, TOTPDevice device) + throws StorageQueryException, DeviceAlreadyExistsException, TenantOrAppNotFoundException { + Connection sqlCon = (Connection) con.getConnection(); + try { + TOTPQueries.createDevice_Transaction(this, sqlCon, appIdentifier, device); + return device; + } catch (SQLException e) { + Exception actualException = e; if (actualException instanceof PSQLException) { ServerErrorMessage errMsg = ((PSQLException) actualException).getServerErrorMessage(); if (isPrimaryKeyError(errMsg, Config.getConfig(this).getTotpUserDevicesTable())) { - throw new DeviceAlreadyExistsException(); + throw new DeviceAlreadyExistsException(); } else if (isForeignKeyConstraintError(errMsg, Config.getConfig(this).getTotpUsersTable(), "app_id")) { - throw new TenantOrAppNotFoundException(appIdentifier); + throw new TenantOrAppNotFoundException(appIdentifier); } - } + throw new StorageQueryException(e); + } + } - throw new StorageQueryException(e.actualException); + @Override + public TOTPDevice getDeviceByName_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId, String deviceName) throws StorageQueryException { + Connection sqlCon = (Connection) con.getConnection(); + try { + return TOTPQueries.getDeviceByName_Transaction(this, sqlCon, appIdentifier, userId, deviceName); + } catch (SQLException e) { + throw new StorageQueryException(e); } } @@ -2706,8 +2738,8 @@ public boolean removeUser(TenantIdentifier tenantIdentifier, String userId) @Override public void updateDeviceName(AppIdentifier appIdentifier, String userId, String oldDeviceName, String newDeviceName) - throws StorageQueryException, DeviceAlreadyExistsException, - UnknownDeviceException { + throws StorageQueryException, + UnknownDeviceException, DeviceAlreadyExistsException { try { int updatedCount = TOTPQueries.updateDeviceName(this, appIdentifier, userId, oldDeviceName, newDeviceName); if (updatedCount == 0) { @@ -2717,7 +2749,7 @@ public void updateDeviceName(AppIdentifier appIdentifier, String userId, String if (e instanceof PSQLException) { ServerErrorMessage errMsg = ((PSQLException) e).getServerErrorMessage(); if (isPrimaryKeyError(errMsg, Config.getConfig(this).getTotpUserDevicesTable())) { - throw new DeviceAlreadyExistsException(); + throw new DeviceAlreadyExistsException(); } } throw new StorageQueryException(e); @@ -2748,7 +2780,7 @@ public TOTPDevice[] getDevices_Transaction(TransactionConnection con, AppIdentif @Override public void insertUsedCode_Transaction(TransactionConnection con, TenantIdentifier tenantIdentifier, TOTPUsedCode usedCodeObj) - throws StorageQueryException, TotpNotEnabledException, UsedCodeAlreadyExistsException, + throws StorageQueryException, UnknownTotpUserIdException, UsedCodeAlreadyExistsException, TenantOrAppNotFoundException { Connection sqlCon = (Connection) con.getConnection(); try { @@ -2760,7 +2792,7 @@ public void insertUsedCode_Transaction(TransactionConnection con, TenantIdentifi throw new UsedCodeAlreadyExistsException(); } else if (isForeignKeyConstraintError(err, Config.getConfig(this).getTotpUsedCodesTable(), "user_id")) { - throw new TotpNotEnabledException(); + throw new UnknownTotpUserIdException(); } else if (isForeignKeyConstraintError(err, Config.getConfig(this).getTotpUsedCodesTable(), "tenant_id")) { throw new TenantOrAppNotFoundException(tenantIdentifier); } @@ -2790,7 +2822,7 @@ public int removeExpiredCodes(TenantIdentifier tenantIdentifier, long expiredBef throw new StorageQueryException(e); } } - + // MFA recipe: @Override public boolean enableFactor(TenantIdentifier tenantIdentifier, String userId, String factor) diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/TOTPQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/TOTPQueries.java index dad5e52d..5be97157 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/TOTPQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/TOTPQueries.java @@ -134,22 +134,27 @@ private static int insertDevice_Transaction(Start start, Connection con, AppIden }); } - public static void createDevice(Start start, AppIdentifier appIdentifier, TOTPDevice device) - throws StorageQueryException, StorageTransactionLogicException { - start.startTransaction(con -> { - Connection sqlCon = (Connection) con.getConnection(); - - try { - insertUser_Transaction(start, sqlCon, appIdentifier, device.userId); - insertDevice_Transaction(start, sqlCon, appIdentifier, device); - sqlCon.commit(); - } catch (SQLException e) { - throw new StorageTransactionLogicException(e); - } + public static void createDevice_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, TOTPDevice device) + throws SQLException, StorageQueryException { + insertUser_Transaction(start, sqlCon, appIdentifier, device.userId); + insertDevice_Transaction(start, sqlCon, appIdentifier, device); + } + + public static TOTPDevice getDeviceByName_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, String userId, String deviceName) + throws SQLException, StorageQueryException { + String QUERY = "SELECT * FROM " + Config.getConfig(start).getTotpUserDevicesTable() + + " WHERE app_id = ? AND user_id = ? AND device_name = ? FOR UPDATE;"; + return execute(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + pst.setString(3, deviceName); + }, result -> { + if (result.next()) { + return TOTPDeviceRowMapper.getInstance().map(result); + } return null; }); - return; } public static int markDeviceAsVerified(Start start, AppIdentifier appIdentifier, String userId, String deviceName) diff --git a/src/test/java/io/supertokens/storage/postgresql/test/DeadlockTest.java b/src/test/java/io/supertokens/storage/postgresql/test/DeadlockTest.java index 3dd0241f..7851e320 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/DeadlockTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/DeadlockTest.java @@ -35,7 +35,7 @@ import io.supertokens.pluginInterface.sqlStorage.SQLStorage.TransactionIsolationLevel; import io.supertokens.pluginInterface.totp.TOTPDevice; import io.supertokens.pluginInterface.totp.TOTPUsedCode; -import io.supertokens.pluginInterface.totp.exception.TotpNotEnabledException; +import io.supertokens.pluginInterface.totp.exception.UnknownTotpUserIdException; import io.supertokens.pluginInterface.totp.exception.UsedCodeAlreadyExistsException; import io.supertokens.pluginInterface.totp.sqlStorage.TOTPSQLStorage; import io.supertokens.storageLayer.StorageLayer; @@ -294,7 +294,7 @@ public void testConcurrentDeleteAndUpdate() throws Exception { try { totpStorage.insertUsedCode_Transaction(con, new TenantIdentifier(null, null, null), code); totpStorage.commitTransaction(con); - } catch (TotpNotEnabledException | UsedCodeAlreadyExistsException e) { + } catch (UnknownTotpUserIdException | UsedCodeAlreadyExistsException e) { // This should not happen throw new StorageTransactionLogicException(e); } catch (TenantOrAppNotFoundException e) { @@ -456,7 +456,7 @@ public void testConcurrentDeleteAndInsert() throws Exception { try { totpStorage.insertUsedCode_Transaction(con, new TenantIdentifier(null, null, null), code); totpStorage.commitTransaction(con); - } catch (TotpNotEnabledException | UsedCodeAlreadyExistsException e) { + } catch (UnknownTotpUserIdException | UsedCodeAlreadyExistsException e) { // This should not happen throw new StorageTransactionLogicException(e); } catch (TenantOrAppNotFoundException e) { @@ -559,7 +559,7 @@ public void testConcurrentDeleteAndInsert() throws Exception { TOTPUsedCode code2 = new TOTPUsedCode("user", "1234", false, nextDay, now + 1); try { totpStorage.insertUsedCode_Transaction(con, new TenantIdentifier(null, null, null), code2); - } catch (TotpNotEnabledException | UsedCodeAlreadyExistsException e) { + } catch (UnknownTotpUserIdException | UsedCodeAlreadyExistsException e) { // This should not happen throw new StorageTransactionLogicException(e); } catch (TenantOrAppNotFoundException e) { @@ -574,7 +574,7 @@ public void testConcurrentDeleteAndInsert() throws Exception { } catch (StorageTransactionLogicException e) { Exception e2 = e.actualException; - if (e2 instanceof TotpNotEnabledException) { + if (e2 instanceof UnknownTotpUserIdException) { t2Failed.set(true); } } catch (StorageQueryException e) { diff --git a/src/test/java/io/supertokens/storage/postgresql/test/StorageLayerTest.java b/src/test/java/io/supertokens/storage/postgresql/test/StorageLayerTest.java index b48324bb..efd4ab33 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/StorageLayerTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/StorageLayerTest.java @@ -16,7 +16,7 @@ import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.totp.TOTPDevice; import io.supertokens.pluginInterface.totp.TOTPUsedCode; -import io.supertokens.pluginInterface.totp.exception.TotpNotEnabledException; +import io.supertokens.pluginInterface.totp.exception.UnknownTotpUserIdException; import io.supertokens.pluginInterface.totp.exception.UsedCodeAlreadyExistsException; import io.supertokens.pluginInterface.totp.sqlStorage.TOTPSQLStorage; import io.supertokens.storageLayer.StorageLayer; @@ -54,7 +54,7 @@ public static void insertUsedCodeUtil(TOTPSQLStorage storage, TOTPUsedCode usedC storage.insertUsedCode_Transaction(con, new TenantIdentifier(null, null, null), usedCode); storage.commitTransaction(con); return null; - } catch (TotpNotEnabledException | UsedCodeAlreadyExistsException e) { + } catch (UnknownTotpUserIdException | UsedCodeAlreadyExistsException e) { throw new StorageTransactionLogicException(e); } catch (TenantOrAppNotFoundException e) { throw new IllegalStateException(e); @@ -62,7 +62,7 @@ public static void insertUsedCodeUtil(TOTPSQLStorage storage, TOTPUsedCode usedC }); } catch (StorageTransactionLogicException e) { Exception actual = e.actualException; - if (actual instanceof TotpNotEnabledException || actual instanceof UsedCodeAlreadyExistsException) { + if (actual instanceof UnknownTotpUserIdException || actual instanceof UsedCodeAlreadyExistsException) { throw actual; } else { throw e; From e547b945155ee742d766cf4bbc703cd336c39b07 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Fri, 29 Sep 2023 15:09:26 +0530 Subject: [PATCH 089/106] fix: queries --- .../supertokens/storage/postgresql/Start.java | 13 +++++++++++++ .../postgresql/queries/ActiveUsersQueries.java | 4 ++-- .../storage/postgresql/queries/MfaQueries.java | 17 +++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index ae423548..256b5a40 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -758,6 +758,13 @@ public boolean isUserIdBeingUsedInNonAuthRecipe(AppIdentifier appIdentifier, Str return false; } else if (className.equals(ActiveUsersStorage.class.getName())) { return ActiveUsersQueries.getLastActiveByUserId(this, appIdentifier, userId) != null; + } else if (className.equals(MfaStorage.class.getName())) { + try { + MultitenancyQueries.getAllTenants(this); + return MfaQueries.listFactors(this, appIdentifier, userId).length > 0; + } catch (SQLException e) { + throw new StorageQueryException(e); + } } else { throw new IllegalStateException("ClassName: " + className + " is not part of NonAuthRecipeStorage"); } @@ -855,6 +862,12 @@ public void addInfoToNonAuthRecipesBasedOnUserId(TenantIdentifier tenantIdentifi } catch (SQLException e) { throw new StorageQueryException(e); } + } else if (className.equals(MfaStorage.class.getName())) { + try { + MfaQueries.enableFactor(this, tenantIdentifier, userId, "emailpassword"); + } catch (SQLException e) { + throw new StorageQueryException(e); + } } else { throw new IllegalStateException("ClassName: " + className + " is not part of NonAuthRecipeStorage"); } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java index d296e908..cf1ad814 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java @@ -106,7 +106,7 @@ public static int countUsersEnabledTotpAndActiveSince(Start start, AppIdentifier } public static int countUsersEnabledMfa(Start start, AppIdentifier appIdentifier) throws SQLException, StorageQueryException { - String QUERY = "SELECT COUNT(*) as total FROM (SELECT DISTINCT user_id FROM " + Config.getConfig(start).getMfaUserFactorsTable() + "WHERE app_id = ?) AS app_mfa_users"; + String QUERY = "SELECT COUNT(*) as total FROM (SELECT DISTINCT user_id FROM " + Config.getConfig(start).getMfaUserFactorsTable() + " WHERE app_id = ?) AS app_mfa_users"; return execute(start, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); @@ -123,7 +123,7 @@ public static int countUsersEnabledMfaAndActiveSince(Start start, AppIdentifier String QUERY = "SELECT COUNT(*) as total FROM (SELECT DISTINCT user_id FROM " + Config.getConfig(start).getMfaUserFactorsTable() + ") AS mfa_users " + "INNER JOIN " + Config.getConfig(start).getUserLastActiveTable() + " AS user_last_active " + "ON mfa_users.user_id = user_last_active.user_id " - + "WHERE user_last_active.app_id = ?" + + "WHERE user_last_active.app_id = ? " + "AND user_last_active.last_active_time >= ?"; return execute(start, QUERY, pst -> { diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java index ca966a21..aa7213e2 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java @@ -72,6 +72,23 @@ public static String[] listFactors(Start start, TenantIdentifier tenantIdentifie }); } + public static String[] listFactors(Start start, AppIdentifier appIdentifier, String userId) + throws StorageQueryException, SQLException { + String QUERY = "SELECT factor_id FROM " + Config.getConfig(start).getMfaUserFactorsTable() + " WHERE app_id = ? AND user_id = ?"; + + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }, result -> { + List factors = new ArrayList<>(); + while (result.next()) { + factors.add(result.getString("factor_id")); + } + + return factors.toArray(String[]::new); + }); + } + public static int disableFactor(Start start, TenantIdentifier tenantIdentifier, String userId, String factorId) throws StorageQueryException, SQLException { String QUERY = "DELETE FROM " + Config.getConfig(start).getMfaUserFactorsTable() + " WHERE app_id = ? AND tenant_id = ? AND user_id = ? AND factor_id = ?"; From 31578e25dc34e5355eb79ab2c0ee90fdb8816e3c Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Tue, 3 Oct 2023 16:52:11 +0530 Subject: [PATCH 090/106] fix: changes as per plugin interface (#163) --- .../java/io/supertokens/storage/postgresql/Start.java | 8 +++++--- .../storage/postgresql/queries/MfaQueries.java | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 256b5a40..e35995bd 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -49,6 +49,7 @@ import io.supertokens.pluginInterface.jwt.exceptions.DuplicateKeyIdException; import io.supertokens.pluginInterface.jwt.sqlstorage.JWTRecipeSQLStorage; import io.supertokens.pluginInterface.mfa.MfaStorage; +import io.supertokens.pluginInterface.mfa.sqlStorage.MfaSQLStorage; import io.supertokens.pluginInterface.multitenancy.*; import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateClientTypeException; import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateTenantException; @@ -107,7 +108,8 @@ public class Start implements SessionSQLStorage, EmailPasswordSQLStorage, EmailVerificationSQLStorage, ThirdPartySQLStorage, JWTRecipeSQLStorage, PasswordlessSQLStorage, UserMetadataSQLStorage, UserRolesSQLStorage, UserIdMappingStorage, - UserIdMappingSQLStorage, MultitenancyStorage, MultitenancySQLStorage, DashboardSQLStorage, TOTPSQLStorage, MfaStorage, ActiveUsersStorage, AuthRecipeSQLStorage { + UserIdMappingSQLStorage, MultitenancyStorage, MultitenancySQLStorage, DashboardSQLStorage, TOTPSQLStorage, MfaStorage, + MfaSQLStorage, ActiveUsersStorage, ActiveUsersSQLStorage, AuthRecipeSQLStorage { // these configs are protected from being modified / viewed by the dev using the SuperTokens // SaaS. If the core is not running in SuperTokens SaaS, this array has no effect. @@ -2876,9 +2878,9 @@ public boolean disableFactor(TenantIdentifier tenantIdentifier, String userId, S } @Override - public boolean deleteMfaInfoForUser(AppIdentifier appIdentifier, String userId) throws StorageQueryException { + public boolean deleteMfaInfoForUser_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId) throws StorageQueryException { try { - int deletedCount = MfaQueries.deleteUser(this, appIdentifier, userId); + int deletedCount = MfaQueries.deleteUser_Transaction(this, (Connection) con.getConnection(), appIdentifier, userId); if (deletedCount == 0) { return false; } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java index aa7213e2..2815043f 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java @@ -22,6 +22,7 @@ import io.supertokens.storage.postgresql.config.Config; import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import java.sql.Connection; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; @@ -101,12 +102,11 @@ public static int disableFactor(Start start, TenantIdentifier tenantIdentifier, }); } - - public static int deleteUser(Start start, AppIdentifier appIdentifier, String userId) + public static int deleteUser_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, String userId) throws StorageQueryException, SQLException { String QUERY = "DELETE FROM " + Config.getConfig(start).getMfaUserFactorsTable() + " WHERE app_id = ? AND user_id = ?"; - return update(start, QUERY, pst -> { + return update(sqlCon, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); pst.setString(2, userId); }); From d3ab41b23ec7cb6ee166c0118df82e828ddc7294 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Tue, 17 Oct 2023 13:56:27 +0530 Subject: [PATCH 091/106] fix: mfa cleanup (#164) * fix: mfa cleanup * fix: pr comments --- .../supertokens/storage/postgresql/Start.java | 84 +----------- .../postgresql/config/PostgreSQLConfig.java | 4 - .../queries/ActiveUsersQueries.java | 28 +--- .../postgresql/queries/GeneralQueries.java | 11 +- .../postgresql/queries/MfaQueries.java | 126 ------------------ 5 files changed, 8 insertions(+), 245 deletions(-) delete mode 100644 src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index e35995bd..fdaca7c9 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -48,8 +48,6 @@ import io.supertokens.pluginInterface.jwt.JWTSigningKeyInfo; import io.supertokens.pluginInterface.jwt.exceptions.DuplicateKeyIdException; import io.supertokens.pluginInterface.jwt.sqlstorage.JWTRecipeSQLStorage; -import io.supertokens.pluginInterface.mfa.MfaStorage; -import io.supertokens.pluginInterface.mfa.sqlStorage.MfaSQLStorage; import io.supertokens.pluginInterface.multitenancy.*; import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateClientTypeException; import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateTenantException; @@ -108,8 +106,8 @@ public class Start implements SessionSQLStorage, EmailPasswordSQLStorage, EmailVerificationSQLStorage, ThirdPartySQLStorage, JWTRecipeSQLStorage, PasswordlessSQLStorage, UserMetadataSQLStorage, UserRolesSQLStorage, UserIdMappingStorage, - UserIdMappingSQLStorage, MultitenancyStorage, MultitenancySQLStorage, DashboardSQLStorage, TOTPSQLStorage, MfaStorage, - MfaSQLStorage, ActiveUsersStorage, ActiveUsersSQLStorage, AuthRecipeSQLStorage { + UserIdMappingSQLStorage, MultitenancyStorage, MultitenancySQLStorage, DashboardSQLStorage, TOTPSQLStorage, + ActiveUsersStorage, ActiveUsersSQLStorage, AuthRecipeSQLStorage { // these configs are protected from being modified / viewed by the dev using the SuperTokens // SaaS. If the core is not running in SuperTokens SaaS, this array has no effect. @@ -760,13 +758,6 @@ public boolean isUserIdBeingUsedInNonAuthRecipe(AppIdentifier appIdentifier, Str return false; } else if (className.equals(ActiveUsersStorage.class.getName())) { return ActiveUsersQueries.getLastActiveByUserId(this, appIdentifier, userId) != null; - } else if (className.equals(MfaStorage.class.getName())) { - try { - MultitenancyQueries.getAllTenants(this); - return MfaQueries.listFactors(this, appIdentifier, userId).length > 0; - } catch (SQLException e) { - throw new StorageQueryException(e); - } } else { throw new IllegalStateException("ClassName: " + className + " is not part of NonAuthRecipeStorage"); } @@ -864,12 +855,6 @@ public void addInfoToNonAuthRecipesBasedOnUserId(TenantIdentifier tenantIdentifi } catch (SQLException e) { throw new StorageQueryException(e); } - } else if (className.equals(MfaStorage.class.getName())) { - try { - MfaQueries.enableFactor(this, tenantIdentifier, userId, "emailpassword"); - } catch (SQLException e) { - throw new StorageQueryException(e); - } } else { throw new IllegalStateException("ClassName: " + className + " is not part of NonAuthRecipeStorage"); } @@ -2838,71 +2823,6 @@ public int removeExpiredCodes(TenantIdentifier tenantIdentifier, long expiredBef } } - // MFA recipe: - @Override - public boolean enableFactor(TenantIdentifier tenantIdentifier, String userId, String factor) - throws StorageQueryException { - try { - int insertedCount = MfaQueries.enableFactor(this, tenantIdentifier, userId, factor); - if (insertedCount == 0) { - return false; - } - return true; - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - - @Override - public String[] listFactors(TenantIdentifier tenantIdentifier, String userId) - throws StorageQueryException { - try { - return MfaQueries.listFactors(this, tenantIdentifier, userId); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - - @Override - public boolean disableFactor(TenantIdentifier tenantIdentifier, String userId, String factor) - throws StorageQueryException { - try { - int deletedCount = MfaQueries.disableFactor(this, tenantIdentifier, userId, factor); - if (deletedCount == 0) { - return false; - } - return true; - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - - @Override - public boolean deleteMfaInfoForUser_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId) throws StorageQueryException { - try { - int deletedCount = MfaQueries.deleteUser_Transaction(this, (Connection) con.getConnection(), appIdentifier, userId); - if (deletedCount == 0) { - return false; - } - return true; - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - - @Override - public boolean deleteMfaInfoForUser(TenantIdentifier tenantIdentifier, String userId) throws StorageQueryException { - try { - int deletedCount = MfaQueries.deleteUser(this, tenantIdentifier, userId); - if (deletedCount == 0) { - return false; - } - return true; - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - @Override public Set getValidFieldsInConfig() { return PostgreSQLConfig.getValidFields(); diff --git a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java index 2a7bd5db..e8bc81d6 100644 --- a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java +++ b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java @@ -302,10 +302,6 @@ public String getTotpUsedCodesTable() { return addSchemaAndPrefixToTableName("totp_used_codes"); } - public String getMfaUserFactorsTable() { - return addSchemaAndPrefixToTableName("mfa_user_factors"); - } - private String addSchemaAndPrefixToTableName(String tableName) { return addSchemaToTableName(postgresql_table_names_prefix + tableName); } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java index cf1ad814..3faeda33 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java @@ -106,35 +106,11 @@ public static int countUsersEnabledTotpAndActiveSince(Start start, AppIdentifier } public static int countUsersEnabledMfa(Start start, AppIdentifier appIdentifier) throws SQLException, StorageQueryException { - String QUERY = "SELECT COUNT(*) as total FROM (SELECT DISTINCT user_id FROM " + Config.getConfig(start).getMfaUserFactorsTable() + " WHERE app_id = ?) AS app_mfa_users"; - - return execute(start, QUERY, pst -> { - pst.setString(1, appIdentifier.getAppId()); - }, result -> { - if (result.next()) { - return result.getInt("total"); - } - return 0; - }); + return 0; // TODO } public static int countUsersEnabledMfaAndActiveSince(Start start, AppIdentifier appIdentifier, long sinceTime) throws SQLException, StorageQueryException { - // Find unique users from mfa_user_factors table and join with user_last_active table - String QUERY = "SELECT COUNT(*) as total FROM (SELECT DISTINCT user_id FROM " + Config.getConfig(start).getMfaUserFactorsTable() + ") AS mfa_users " - + "INNER JOIN " + Config.getConfig(start).getUserLastActiveTable() + " AS user_last_active " - + "ON mfa_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; - }); + return 0; // TODO } public static int updateUserLastActive(Start start, AppIdentifier appIdentifier, String userId) diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java index 8be26c54..dcd36eec 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java @@ -28,6 +28,7 @@ import io.supertokens.storage.postgresql.ConnectionPool; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.config.Config; +import io.supertokens.storage.postgresql.queries.GeneralQueries.AccountLinkingInfo; import io.supertokens.storage.postgresql.utils.Utils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -516,11 +517,6 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto update(start, TOTPQueries.getQueryToCreateTenantIdIndexForUsedCodesTable(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getMfaUserFactorsTable())) { - getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, MfaQueries.getQueryToCreateUserFactorsTable(start), NO_OP_SETTER); - } - } catch (Exception e) { if (e.getMessage().contains("schema") && e.getMessage().contains("does not exist") && numberOfRetries < 1) { @@ -589,8 +585,9 @@ public static void deleteAllTables(Start start) throws SQLException, StorageQuer + getConfig(start).getUserRolesTable() + "," + getConfig(start).getDashboardUsersTable() + "," + getConfig(start).getDashboardSessionsTable() + "," - + getConfig(start).getTotpUsedCodesTable() + "," + getConfig(start).getTotpUserDevicesTable() + "," - + getConfig(start).getTotpUsersTable() + "," + getConfig(start).getMfaUserFactorsTable(); + + getConfig(start).getTotpUsedCodesTable() + "," + + getConfig(start).getTotpUserDevicesTable() + "," + + getConfig(start).getTotpUsersTable(); update(start, DROP_QUERY, NO_OP_SETTER); } } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java deleted file mode 100644 index 2815043f..00000000 --- a/src/main/java/io/supertokens/storage/postgresql/queries/MfaQueries.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * 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.storage.postgresql.queries; - -import io.supertokens.pluginInterface.multitenancy.AppIdentifier; -import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; -import io.supertokens.storage.postgresql.Start; -import io.supertokens.storage.postgresql.config.Config; -import io.supertokens.pluginInterface.exceptions.StorageQueryException; - -import java.sql.Connection; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; - -import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; -import static io.supertokens.storage.postgresql.QueryExecutorTemplate.update; - -public class MfaQueries { - public static String getQueryToCreateUserFactorsTable(Start start) { - return "CREATE TABLE IF NOT EXISTS " + Config.getConfig(start).getMfaUserFactorsTable() + " (" - + "app_id VARCHAR(64) DEFAULT 'public'," - + "tenant_id VARCHAR(64) DEFAULT 'public'," - + "user_id VARCHAR(128) NOT NULL," - + "factor_id VARCHAR(64) NOT NULL," - + "PRIMARY KEY (app_id, tenant_id, user_id, factor_id)," - + "FOREIGN KEY (app_id, tenant_id)" - + "REFERENCES " + Config.getConfig(start).getTenantsTable() + " (app_id, tenant_id) ON DELETE CASCADE);"; - } - - public static int enableFactor(Start start, TenantIdentifier tenantIdentifier, String userId, String factorId) - throws StorageQueryException, SQLException { - String QUERY = "INSERT INTO " + Config.getConfig(start).getMfaUserFactorsTable() + " (app_id, tenant_id, user_id, factor_id) VALUES (?, ?, ?, ?) ON CONFLICT DO NOTHING"; - - return update(start, QUERY, pst -> { - pst.setString(1, tenantIdentifier.getAppId()); - pst.setString(2, tenantIdentifier.getTenantId()); - pst.setString(3, userId); - pst.setString(4, factorId); - }); - } - - - public static String[] listFactors(Start start, TenantIdentifier tenantIdentifier, String userId) - throws StorageQueryException, SQLException { - String QUERY = "SELECT factor_id FROM " + Config.getConfig(start).getMfaUserFactorsTable() + " WHERE app_id = ? AND tenant_id = ? AND user_id = ?"; - - return execute(start, QUERY, pst -> { - pst.setString(1, tenantIdentifier.getAppId()); - pst.setString(2, tenantIdentifier.getTenantId()); - pst.setString(3, userId); - }, result -> { - List factors = new ArrayList<>(); - while (result.next()) { - factors.add(result.getString("factor_id")); - } - - return factors.toArray(String[]::new); - }); - } - - public static String[] listFactors(Start start, AppIdentifier appIdentifier, String userId) - throws StorageQueryException, SQLException { - String QUERY = "SELECT factor_id FROM " + Config.getConfig(start).getMfaUserFactorsTable() + " WHERE app_id = ? AND user_id = ?"; - - return execute(start, QUERY, pst -> { - pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, userId); - }, result -> { - List factors = new ArrayList<>(); - while (result.next()) { - factors.add(result.getString("factor_id")); - } - - return factors.toArray(String[]::new); - }); - } - - public static int disableFactor(Start start, TenantIdentifier tenantIdentifier, String userId, String factorId) - throws StorageQueryException, SQLException { - String QUERY = "DELETE FROM " + Config.getConfig(start).getMfaUserFactorsTable() + " WHERE app_id = ? AND tenant_id = ? AND user_id = ? AND factor_id = ?"; - - return update(start, QUERY, pst -> { - pst.setString(1, tenantIdentifier.getAppId()); - pst.setString(2, tenantIdentifier.getTenantId()); - pst.setString(3, userId); - pst.setString(4, factorId); - }); - } - - public static int deleteUser_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, String userId) - throws StorageQueryException, SQLException { - String QUERY = "DELETE FROM " + Config.getConfig(start).getMfaUserFactorsTable() + " WHERE app_id = ? AND user_id = ?"; - - return update(sqlCon, QUERY, pst -> { - pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, userId); - }); - } - - public static int deleteUser(Start start, TenantIdentifier tenantIdentifier, String userId) - throws StorageQueryException, SQLException { - String QUERY = "DELETE FROM " + Config.getConfig(start).getMfaUserFactorsTable() + " WHERE app_id = ? AND tenant_id = ? AND user_id = ?"; - - return update(start, QUERY, pst -> { - pst.setString(1, tenantIdentifier.getAppId()); - pst.setString(2, tenantIdentifier.getTenantId()); - pst.setString(3, userId); - }); - } - -} From b3f7a73161a72d5a3c558e75b4bee3eeb270fc2c Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Fri, 27 Oct 2023 13:04:41 +0530 Subject: [PATCH 092/106] Mfa multitenancy (#167) * fix: mfa multitenancy queries * fix: mfa cleanup * fix: mfa config storage * fix: mfa * fix: tests * fix: default values * fix: pr comments * fix: pr comments * fix: minor fix * fix: pr comments * fix: set * fix: pr comment * fix: constraint --- .../postgresql/config/PostgreSQLConfig.java | 8 ++ .../postgresql/queries/GeneralQueries.java | 25 +++- .../queries/MultitenancyQueries.java | 70 +++++++++- .../queries/multitenancy/MfaSqlHelper.java | 120 ++++++++++++++++++ .../multitenancy/TenantConfigSQLHelper.java | 37 +++++- .../storage/postgresql/utils/Utils.java | 9 ++ .../postgresql/test/AccountLinkingTests.java | 2 + .../storage/postgresql/test/LoggingTest.java | 1 + .../test/SuperTokensSaaSSecretTest.java | 3 + .../test/multitenancy/StorageLayerTest.java | 21 +++ .../TestUserPoolIdChangeBehaviour.java | 4 + 11 files changed, 290 insertions(+), 10 deletions(-) create mode 100644 src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/MfaSqlHelper.java diff --git a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java index e8bc81d6..27023cdf 100644 --- a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java +++ b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java @@ -182,6 +182,14 @@ public String getTenantConfigsTable() { return addSchemaAndPrefixToTableName("tenant_configs"); } + public String getTenantFirstFactorsTable() { + return addSchemaAndPrefixToTableName("tenant_first_factors"); + } + + public String getTenantDefaultRequiredFactorIdsTable() { + return addSchemaAndPrefixToTableName("tenant_default_required_factor_ids"); + } + public String getTenantThirdPartyProvidersTable() { return addSchemaAndPrefixToTableName("tenant_thirdparty_providers"); } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java index 0dfe64b6..0c1bbc07 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java @@ -28,7 +28,6 @@ import io.supertokens.storage.postgresql.ConnectionPool; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.config.Config; -import io.supertokens.storage.postgresql.queries.GeneralQueries.AccountLinkingInfo; import io.supertokens.storage.postgresql.utils.Utils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -322,6 +321,28 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto NO_OP_SETTER); } + if (!doesTableExists(start, Config.getConfig(start).getTenantFirstFactorsTable())) { + getInstance(start).addState(CREATING_NEW_TABLE, null); + update(start, MultitenancyQueries.getQueryToCreateFirstFactorsTable(start), NO_OP_SETTER); + + // index + update(start, MultitenancyQueries.getQueryToCreateTenantIdIndexForFirstFactorsTable(start), + NO_OP_SETTER); + } + + if (!doesTableExists(start, Config.getConfig(start).getTenantDefaultRequiredFactorIdsTable())) { + getInstance(start).addState(CREATING_NEW_TABLE, null); + update(start, MultitenancyQueries.getQueryToCreateDefaultRequiredFactorIdsTable(start), NO_OP_SETTER); + + // index + update(start, + MultitenancyQueries.getQueryToCreateTenantIdIndexForDefaultRequiredFactorIdsTable(start), + NO_OP_SETTER); + update(start, + MultitenancyQueries.getQueryToCreateOrderIndexForDefaultRequiredFactorIdsTable(start), + NO_OP_SETTER); + } + if (!doesTableExists(start, Config.getConfig(start).getTenantThirdPartyProviderClientsTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); update(start, MultitenancyQueries.getQueryToCreateTenantThirdPartyProviderClientsTable(start), @@ -563,6 +584,8 @@ public static void deleteAllTables(Start start) throws SQLException, StorageQuer + getConfig(start).getUserIdMappingTable() + "," + getConfig(start).getUsersTable() + "," + getConfig(start).getAccessTokenSigningKeysTable() + "," + + getConfig(start).getTenantFirstFactorsTable() + "," + + getConfig(start).getTenantDefaultRequiredFactorIdsTable() + "," + getConfig(start).getTenantConfigsTable() + "," + getConfig(start).getTenantThirdPartyProvidersTable() + "," + getConfig(start).getTenantThirdPartyProviderClientsTable() + "," diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/MultitenancyQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/MultitenancyQueries.java index 592cdbd0..5a0c8065 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/MultitenancyQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/MultitenancyQueries.java @@ -16,13 +16,13 @@ package io.supertokens.storage.postgresql.queries; -import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.*; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.config.Config; +import io.supertokens.storage.postgresql.queries.multitenancy.MfaSqlHelper; import io.supertokens.storage.postgresql.queries.multitenancy.TenantConfigSQLHelper; import io.supertokens.storage.postgresql.queries.multitenancy.ThirdPartyProviderClientSQLHelper; import io.supertokens.storage.postgresql.queries.multitenancy.ThirdPartyProviderSQLHelper; @@ -49,6 +49,9 @@ static String getQueryToCreateTenantConfigsTable(Start start) { + "email_password_enabled BOOLEAN," + "passwordless_enabled BOOLEAN," + "third_party_enabled BOOLEAN," + + "totp_enabled BOOLEAN," + + "has_first_factors BOOLEAN DEFAULT FALSE," + + "has_default_required_factor_ids BOOLEAN DEFAULT FALSE," + "CONSTRAINT " + Utils.getConstraintName(schema, tenantConfigsTable, null, "pkey") + " PRIMARY KEY (connection_uri_domain, app_id, tenant_id)" + ");"; // @formatter:on @@ -119,6 +122,60 @@ public static String getQueryToCreateThirdPartyIdIndexForTenantThirdPartyProvide + getConfig(start).getTenantThirdPartyProviderClientsTable() + " (connection_uri_domain, app_id, tenant_id, third_party_id);"; } + public static String getQueryToCreateFirstFactorsTable(Start start) { + String schema = Config.getConfig(start).getTableSchema(); + String tableName = Config.getConfig(start).getTenantFirstFactorsTable(); + // @formatter:off + return "CREATE TABLE IF NOT EXISTS " + tableName + " (" + + "connection_uri_domain VARCHAR(256) DEFAULT ''," + + "app_id VARCHAR(64) DEFAULT 'public'," + + "tenant_id VARCHAR(64) DEFAULT 'public'," + + "factor_id VARCHAR(128)," + + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, null, "pkey") + + " PRIMARY KEY (connection_uri_domain, app_id, tenant_id, factor_id)," + + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, "tenant_id", "fkey") + + " FOREIGN KEY (connection_uri_domain, app_id, tenant_id)" + + " REFERENCES " + Config.getConfig(start).getTenantConfigsTable() + " (connection_uri_domain, app_id, tenant_id) ON DELETE CASCADE" + + ");"; + // @formatter:on + } + + public static String getQueryToCreateTenantIdIndexForFirstFactorsTable(Start start) { + return "CREATE INDEX IF NOT EXISTS tenant_first_factors_tenant_id_index ON " + + getConfig(start).getTenantFirstFactorsTable() + " (connection_uri_domain, app_id, tenant_id);"; + } + + public static String getQueryToCreateDefaultRequiredFactorIdsTable(Start start) { + String schema = Config.getConfig(start).getTableSchema(); + String tableName = Config.getConfig(start).getTenantDefaultRequiredFactorIdsTable(); + // @formatter:off + return "CREATE TABLE IF NOT EXISTS " + tableName + " (" + + "connection_uri_domain VARCHAR(256) DEFAULT ''," + + "app_id VARCHAR(64) DEFAULT 'public'," + + "tenant_id VARCHAR(64) DEFAULT 'public'," + + "factor_id VARCHAR(128)," + + "order_idx INTEGER NOT NULL," + + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, null, "pkey") + + " PRIMARY KEY (connection_uri_domain, app_id, tenant_id, factor_id)," + + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, "tenant_id", "fkey") + + " FOREIGN KEY (connection_uri_domain, app_id, tenant_id)" + + " REFERENCES " + Config.getConfig(start).getTenantConfigsTable() + " (connection_uri_domain, app_id, tenant_id) ON DELETE CASCADE," + + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, "order_idx", "key") + + " UNIQUE (connection_uri_domain, app_id, tenant_id, order_idx)" + + ");"; + // @formatter:on + } + + public static String getQueryToCreateTenantIdIndexForDefaultRequiredFactorIdsTable(Start start) { + return "CREATE INDEX IF NOT EXISTS tenant_default_required_factor_ids_tenant_id_index ON " + + getConfig(start).getTenantDefaultRequiredFactorIdsTable() + " (connection_uri_domain, app_id, tenant_id);"; + } + + public static String getQueryToCreateOrderIndexForDefaultRequiredFactorIdsTable(Start start) { + return "CREATE INDEX IF NOT EXISTS tenant_default_required_factor_ids_tenant_id_index ON " + + getConfig(start).getTenantDefaultRequiredFactorIdsTable() + " (order_idx ASC);"; + } + private static void executeCreateTenantQueries(Start start, Connection sqlCon, TenantConfig tenantConfig) throws SQLException, StorageQueryException { @@ -131,6 +188,9 @@ private static void executeCreateTenantQueries(Start start, Connection sqlCon, T ThirdPartyProviderClientSQLHelper.create(start, sqlCon, tenantConfig, provider, providerClient); } } + + MfaSqlHelper.createFirstFactors(start, sqlCon, tenantConfig.tenantIdentifier, tenantConfig.firstFactors); + MfaSqlHelper.createDefaultRequiredFactorIds(start, sqlCon, tenantConfig.tenantIdentifier, tenantConfig.defaultRequiredFactorIds); } public static void createTenantConfig(Start start, TenantConfig tenantConfig) throws StorageQueryException, StorageTransactionLogicException { @@ -209,7 +269,13 @@ public static TenantConfig[] getAllTenants(Start start) throws StorageQueryExcep // Map (tenantIdentifier) -> thirdPartyId -> provider HashMap> providerMap = ThirdPartyProviderSQLHelper.selectAll(start, providerClientsMap); - return TenantConfigSQLHelper.selectAll(start, providerMap); + // Map (tenantIdentifier) -> firstFactors + HashMap firstFactorsMap = MfaSqlHelper.selectAllFirstFactors(start); + + // Map (tenantIdentifier) -> defaultRequiredFactorIds + HashMap defaultRequiredFactorIdsMap = MfaSqlHelper.selectAllDefaultRequiredFactorIds(start); + + return TenantConfigSQLHelper.selectAll(start, providerMap, firstFactorsMap, defaultRequiredFactorIdsMap); } catch (SQLException throwables) { throw new StorageQueryException(throwables); } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/MfaSqlHelper.java b/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/MfaSqlHelper.java new file mode 100644 index 00000000..2157c248 --- /dev/null +++ b/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/MfaSqlHelper.java @@ -0,0 +1,120 @@ +/* + * 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.storage.postgresql.queries.multitenancy; + +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.storage.postgresql.Start; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.*; + +import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; +import static io.supertokens.storage.postgresql.QueryExecutorTemplate.update; +import static io.supertokens.storage.postgresql.config.Config.getConfig; + +public class MfaSqlHelper { + public static HashMap selectAllFirstFactors(Start start) + throws SQLException, StorageQueryException { + String QUERY = "SELECT connection_uri_domain, app_id, tenant_id, factor_id FROM " + + getConfig(start).getTenantFirstFactorsTable() + ";"; + return execute(start, QUERY, pst -> {}, result -> { + HashMap> firstFactors = new HashMap<>(); + + while (result.next()) { + TenantIdentifier tenantIdentifier = new TenantIdentifier(result.getString("connection_uri_domain"), result.getString("app_id"), result.getString("tenant_id")); + if (!firstFactors.containsKey(tenantIdentifier)) { + firstFactors.put(tenantIdentifier, new ArrayList<>()); + } + + firstFactors.get(tenantIdentifier).add(result.getString("factor_id")); + } + + HashMap finalResult = new HashMap<>(); + for (TenantIdentifier tenantIdentifier : firstFactors.keySet()) { + finalResult.put(tenantIdentifier, firstFactors.get(tenantIdentifier).toArray(new String[0])); + } + + return finalResult; + }); + } + + public static HashMap selectAllDefaultRequiredFactorIds(Start start) + throws SQLException, StorageQueryException { + String QUERY = "SELECT connection_uri_domain, app_id, tenant_id, factor_id, order_idx FROM " + + getConfig(start).getTenantDefaultRequiredFactorIdsTable() + " ORDER BY order_idx ASC;"; + return execute(start, QUERY, pst -> {}, result -> { + HashMap> defaultRequiredFactors = new HashMap<>(); + + while (result.next()) { + TenantIdentifier tenantIdentifier = new TenantIdentifier(result.getString("connection_uri_domain"), + result.getString("app_id"), result.getString("tenant_id")); + if (!defaultRequiredFactors.containsKey(tenantIdentifier)) { + defaultRequiredFactors.put(tenantIdentifier, new ArrayList<>()); + } + + defaultRequiredFactors.get(tenantIdentifier).add(result.getString("factor_id")); + } + + HashMap finalResult = new HashMap<>(); + for (TenantIdentifier tenantIdentifier : defaultRequiredFactors.keySet()) { + finalResult.put(tenantIdentifier, defaultRequiredFactors.get(tenantIdentifier).toArray(new String[0])); + } + + return finalResult; + }); + } + + public static void createFirstFactors(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String[] firstFactors) + throws SQLException, StorageQueryException { + if (firstFactors == null || firstFactors.length == 0) { + return; + } + + String QUERY = "INSERT INTO " + getConfig(start).getTenantFirstFactorsTable() + "(connection_uri_domain, app_id, tenant_id, factor_id) VALUES (?, ?, ?, ?);"; + for (String factorId : new HashSet<>(Arrays.asList(firstFactors))) { + update(sqlCon, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getConnectionUriDomain()); + pst.setString(2, tenantIdentifier.getAppId()); + pst.setString(3, tenantIdentifier.getTenantId()); + pst.setString(4, factorId); + }); + } + } + + public static void createDefaultRequiredFactorIds(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String[] defaultRequiredFactorIds) + throws SQLException, StorageQueryException { + if (defaultRequiredFactorIds == null || defaultRequiredFactorIds.length == 0) { + return; + } + + String QUERY = "INSERT INTO " + getConfig(start).getTenantDefaultRequiredFactorIdsTable() + "(connection_uri_domain, app_id, tenant_id, factor_id, order_idx) VALUES (?, ?, ?, ?, ?);"; + int orderIdx = 0; + for (String factorId : defaultRequiredFactorIds) { + int finalOrderIdx = orderIdx; + update(sqlCon, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getConnectionUriDomain()); + pst.setString(2, tenantIdentifier.getAppId()); + pst.setString(3, tenantIdentifier.getTenantId()); + pst.setString(4, factorId); + pst.setInt(5, finalOrderIdx); + }); + orderIdx++; + } + } +} diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/TenantConfigSQLHelper.java b/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/TenantConfigSQLHelper.java index 0dfadb9b..7d37d531 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/TenantConfigSQLHelper.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/TenantConfigSQLHelper.java @@ -16,11 +16,15 @@ package io.supertokens.storage.postgresql.queries.multitenancy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; import io.supertokens.pluginInterface.RowMapper; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.*; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.queries.utils.JsonUtils; +import io.supertokens.storage.postgresql.utils.Utils; import java.sql.Connection; import java.sql.ResultSet; @@ -36,13 +40,17 @@ public class TenantConfigSQLHelper { public static class TenantConfigRowMapper implements RowMapper { ThirdPartyConfig.Provider[] providers; + String[] firstFactors; + String[] defaultRequiredFactorIds; - private TenantConfigRowMapper(ThirdPartyConfig.Provider[] providers) { + private TenantConfigRowMapper(ThirdPartyConfig.Provider[] providers, String[] firstFactors, String[] defaultRequiredFactorIds) { this.providers = providers; + this.firstFactors = firstFactors; + this.defaultRequiredFactorIds = defaultRequiredFactorIds; } - public static TenantConfigSQLHelper.TenantConfigRowMapper getInstance(ThirdPartyConfig.Provider[] providers) { - return new TenantConfigSQLHelper.TenantConfigRowMapper(providers); + public static TenantConfigSQLHelper.TenantConfigRowMapper getInstance(ThirdPartyConfig.Provider[] providers, String[] firstFactors, String[] defaultRequiredFactorIds) { + return new TenantConfigSQLHelper.TenantConfigRowMapper(providers, firstFactors, defaultRequiredFactorIds); } @Override @@ -53,6 +61,9 @@ public TenantConfig map(ResultSet result) throws StorageQueryException { new EmailPasswordConfig(result.getBoolean("email_password_enabled")), new ThirdPartyConfig(result.getBoolean("third_party_enabled"), this.providers), new PasswordlessConfig(result.getBoolean("passwordless_enabled")), + new TotpConfig(result.getBoolean("totp_enabled")), + result.getBoolean("has_first_factors") ? firstFactors : null, + result.getBoolean("has_default_required_factor_ids") ? defaultRequiredFactorIds : null, JsonUtils.stringToJsonObject(result.getString("core_config")) ); } catch (Exception e) { @@ -61,9 +72,11 @@ public TenantConfig map(ResultSet result) throws StorageQueryException { } } - public static TenantConfig[] selectAll(Start start, HashMap> providerMap) + public static TenantConfig[] selectAll(Start start, HashMap> providerMap, HashMap firstFactorsMap, HashMap defaultRequiredFactorIdsMap) throws SQLException, StorageQueryException { - String QUERY = "SELECT connection_uri_domain, app_id, tenant_id, core_config, email_password_enabled, passwordless_enabled, third_party_enabled FROM " + String QUERY = "SELECT connection_uri_domain, app_id, tenant_id, core_config," + + " email_password_enabled, passwordless_enabled, third_party_enabled," + + " totp_enabled, has_first_factors, has_default_required_factor_ids FROM " + getConfig(start).getTenantConfigsTable() + ";"; TenantConfig[] tenantConfigs = execute(start, QUERY, pst -> {}, result -> { @@ -74,7 +87,11 @@ public static TenantConfig[] selectAll(Start start, HashMap { pst.setString(1, tenantConfig.tenantIdentifier.getConnectionUriDomain()); @@ -98,6 +118,9 @@ public static void create(Start start, Connection sqlCon, TenantConfig tenantCon pst.setBoolean(5, tenantConfig.emailPasswordConfig.enabled); pst.setBoolean(6, tenantConfig.passwordlessConfig.enabled); pst.setBoolean(7, tenantConfig.thirdPartyConfig.enabled); + pst.setBoolean(8, tenantConfig.totpConfig.enabled); + pst.setBoolean(9, tenantConfig.firstFactors != null); + pst.setBoolean(10, tenantConfig.defaultRequiredFactorIds != null); }); } diff --git a/src/main/java/io/supertokens/storage/postgresql/utils/Utils.java b/src/main/java/io/supertokens/storage/postgresql/utils/Utils.java index 91a58735..5f79f42c 100644 --- a/src/main/java/io/supertokens/storage/postgresql/utils/Utils.java +++ b/src/main/java/io/supertokens/storage/postgresql/utils/Utils.java @@ -17,6 +17,8 @@ package io.supertokens.storage.postgresql.utils; +import com.google.gson.Gson; + import java.io.ByteArrayOutputStream; import java.io.PrintStream; @@ -53,4 +55,11 @@ public static String generateCommaSeperatedQuestionMarks(int size) { } return builder.toString(); } + + public static String[] getStringArrayFromJsonString(String input) { + if (input == null) { + return null; + } + return new Gson().fromJson(input, String[].class); + } } diff --git a/src/test/java/io/supertokens/storage/postgresql/test/AccountLinkingTests.java b/src/test/java/io/supertokens/storage/postgresql/test/AccountLinkingTests.java index 4f26a52c..64d98e22 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/AccountLinkingTests.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/AccountLinkingTests.java @@ -88,6 +88,7 @@ public void canLinkFailsIfTryingToLinkUsersAcrossDifferentStorageLayers() throws new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + new TotpConfig(false), null, null, coreConfig ) ); @@ -130,6 +131,7 @@ public void canLinkFailsIfTryingToLinkUsersAcrossDifferentStorageLayers() throws new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + new TotpConfig(false), null, null, coreConfig ) ); diff --git a/src/test/java/io/supertokens/storage/postgresql/test/LoggingTest.java b/src/test/java/io/supertokens/storage/postgresql/test/LoggingTest.java index b27c1ac5..b9c65712 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/LoggingTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/LoggingTest.java @@ -284,6 +284,7 @@ public void confirmHikariLoggerClosedOnlyWhenProcessEnds() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + new TotpConfig(false), null, null, config ), false); diff --git a/src/test/java/io/supertokens/storage/postgresql/test/SuperTokensSaaSSecretTest.java b/src/test/java/io/supertokens/storage/postgresql/test/SuperTokensSaaSSecretTest.java index 17b6e437..c6c7a376 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/SuperTokensSaaSSecretTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/SuperTokensSaaSSecretTest.java @@ -89,6 +89,7 @@ public void testThatTenantCannotSetDatabaseRelatedConfigIfSuperTokensSaaSSecretI Multitenancy.addNewOrUpdateAppOrTenant(process.main, new TenantConfig(new TenantIdentifier(null, null, "t1"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), + new TotpConfig(false), null, null, j), true); fail(); } catch (BadPermissionException e) { @@ -165,6 +166,7 @@ public void testThatTenantCanSetDatabaseRelatedConfigIfSuperTokensSaaSSecretIsNo new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), + new TotpConfig(false), null, null, j), false); } @@ -217,6 +219,7 @@ public void testThatTenantCannotGetDatabaseRelatedConfigIfSuperTokensSaaSSecretI new TenantConfig(new TenantIdentifier(null, null, "t" + i), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), + new TotpConfig(false), null, null, j)); { diff --git a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java index afce4e11..572ea0cd 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java @@ -112,6 +112,7 @@ public void mergingTenantWithBaseConfigWorks() new TenantConfig(new TenantIdentifier("abc", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), + new TotpConfig(false), null, null, tenantConfig)}; Config.loadAllTenantConfig(process.getProcess(), tenants); @@ -162,6 +163,7 @@ public void storageInstanceIsReusedAcrossTenants() new TenantConfig(new TenantIdentifier(null, "abc", null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), + new TotpConfig(false), null, null, tenantConfig)}; Config.loadAllTenantConfig(process.getProcess(), tenants); @@ -209,14 +211,17 @@ public void storageInstanceIsReusedAcrossTenantsComplex() new TenantConfig(new TenantIdentifier(null, "abc", null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), + new TotpConfig(false), null, null, tenantConfig), new TenantConfig(new TenantIdentifier(null, "abc", "t1"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), + new TotpConfig(false), null, null, tenantConfig1), new TenantConfig(new TenantIdentifier(null, null, "t2"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), + new TotpConfig(false), null, null, tenantConfig1)}; Config.loadAllTenantConfig(process.getProcess(), tenants); @@ -281,6 +286,7 @@ public void mergingTenantWithBaseConfigWithInvalidConfigThrowsErrorWorks() new TenantConfig(new TenantIdentifier("abc", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), + new TotpConfig(false), null, null, tenantConfig)}; Config.loadAllTenantConfig(process.getProcess(), tenants); @@ -314,6 +320,7 @@ public void mergingTenantWithBaseConfigWithConflictingConfigsThrowsError() new TenantConfig(new TenantIdentifier(null, "abc", null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), + new TotpConfig(false), null, null, tenantConfig)}; Config.loadAllTenantConfig(process.getProcess(), tenants); @@ -348,6 +355,7 @@ public void mergingDifferentConnectionPoolIdTenantWithBaseConfigWithConflictingC new TenantConfig(new TenantIdentifier(null, "abc", null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), + new TotpConfig(false), null, null, tenantConfig)}; Config.loadAllTenantConfig(process.getProcess(), tenants); @@ -383,6 +391,7 @@ public void mergingDifferentUserPoolIdTenantWithBaseConfigWithConflictingConfigs new TenantConfig(new TenantIdentifier("abc", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), + new TotpConfig(false), null, null, tenantConfig)}; Config.loadAllTenantConfig(process.getProcess(), tenants); @@ -433,6 +442,7 @@ public void newStorageIsNotCreatedWhenSameTenantIsAdded() new TenantConfig(new TenantIdentifier(null, "abc", null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), + new TotpConfig(false), null, null, tenantConfig)}; Config.loadAllTenantConfig(process.getProcess(), tenants); @@ -480,6 +490,7 @@ public void testDifferentWaysToGetConfigBasedOnConnectionURIAndTenantId() tenants[0] = new TenantConfig(new TenantIdentifier("c1", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), + new TotpConfig(false), null, null, tenantConfig); } @@ -491,6 +502,7 @@ public void testDifferentWaysToGetConfigBasedOnConnectionURIAndTenantId() tenants[1] = new TenantConfig(new TenantIdentifier("c1", null, "t1"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), + new TotpConfig(false), null, null, tenantConfig); } @@ -500,6 +512,7 @@ public void testDifferentWaysToGetConfigBasedOnConnectionURIAndTenantId() tenants[2] = new TenantConfig(new TenantIdentifier(null, null, "t2"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), + new TotpConfig(false), null, null, tenantConfig); } @@ -509,6 +522,7 @@ public void testDifferentWaysToGetConfigBasedOnConnectionURIAndTenantId() tenants[3] = new TenantConfig(new TenantIdentifier(null, null, "t1"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), + new TotpConfig(false), null, null, tenantConfig); } @@ -572,6 +586,7 @@ public void differentUserPoolCreatedBasedOnSchemaInConnectionUri() new TenantConfig(new TenantIdentifier("abc", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), + new TotpConfig(false), null, null, tenantConfig)}; Config.loadAllTenantConfig(process.getProcess(), tenants); @@ -614,6 +629,7 @@ public void multipleTenantsSameUserPoolAndConnectionPoolShouldWork() new TenantConfig(new TenantIdentifier(null, "abc", null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), + new TotpConfig(false), null, null, tenantConfig)}; Config.loadAllTenantConfig(process.getProcess(), tenants); @@ -649,6 +665,7 @@ public void multipleTenantsSameUserPoolAndDifferentConnectionPoolShouldWork() new TenantConfig(new TenantIdentifier(null, "abc", null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), + new TotpConfig(false), null, null, tenantConfig)}; Config.loadAllTenantConfig(process.getProcess(), tenants); @@ -688,6 +705,7 @@ public void testCreating50StorageLayersUsage() new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), + new TotpConfig(false), null, null, config); try { Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantIdentifier(null, null, null), @@ -741,6 +759,7 @@ public void testCantCreateTenantWithUnknownDb() new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), + new TotpConfig(false), null, null, tenantConfigJson); try { @@ -782,6 +801,7 @@ public void testTenantCreationAndThenDbDownDbThrowsErrorInRecipesAndDoesntAffect new EmailPasswordConfig(true), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), + new TotpConfig(false), null, null, tenantConfigJson); StorageLayer.getMultitenancyStorage(process.getProcess()).createTenant(tenantConfig); @@ -862,6 +882,7 @@ public void testBadPortWithNewTenantShouldNotCauseItToWaitInput() throws Excepti new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), + new TotpConfig(false), null, null, tenantConfigJson); try { diff --git a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/TestUserPoolIdChangeBehaviour.java b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/TestUserPoolIdChangeBehaviour.java index 5a1d7a1f..43811fce 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/TestUserPoolIdChangeBehaviour.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/TestUserPoolIdChangeBehaviour.java @@ -83,6 +83,7 @@ public void testUsersWorkAfterUserPoolIdChanges() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + new TotpConfig(false), null, null, coreConfig ), false); @@ -100,6 +101,7 @@ public void testUsersWorkAfterUserPoolIdChanges() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + new TotpConfig(false), null, null, coreConfig ), false); @@ -127,6 +129,7 @@ public void testUsersWorkAfterUserPoolIdChangesAndServerRestart() throws Excepti new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + new TotpConfig(false), null, null, coreConfig ), false); @@ -144,6 +147,7 @@ public void testUsersWorkAfterUserPoolIdChangesAndServerRestart() throws Excepti new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + new TotpConfig(false), null, null, coreConfig ), false); From 6ebaa4793f67b4919bb70589c1f4b21b3a9bf2b4 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Tue, 31 Oct 2023 11:50:31 +0530 Subject: [PATCH 093/106] fix: created_at in totp devices (#169) * fix: created_at in totp devices * fix: add createdat to totp device --- src/main/java/io/supertokens/storage/postgresql/Start.java | 2 +- .../storage/postgresql/queries/TOTPQueries.java | 7 +++++-- .../supertokens/storage/postgresql/test/DeadlockTest.java | 4 ++-- .../storage/postgresql/test/StorageLayerTest.java | 2 +- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index fdaca7c9..ae43d800 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -829,7 +829,7 @@ public void addInfoToNonAuthRecipesBasedOnUserId(TenantIdentifier tenantIdentifi } } else if (className.equals(TOTPStorage.class.getName())) { try { - TOTPDevice device = new TOTPDevice(userId, "testDevice", "secret", 0, 30, false); + TOTPDevice device = new TOTPDevice(userId, "testDevice", "secret", 0, 30, false, System.currentTimeMillis()); this.startTransaction(con -> { try { long now = System.currentTimeMillis(); diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/TOTPQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/TOTPQueries.java index 5be97157..8a8269d5 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/TOTPQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/TOTPQueries.java @@ -54,6 +54,7 @@ public static String getQueryToCreateUserDevicesTable(Start start) { + "period INTEGER NOT NULL," + "skew INTEGER NOT NULL," + "verified BOOLEAN NOT NULL," + + "created_at BIGINT," + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, null, "pkey") + " PRIMARY KEY (app_id, user_id, device_name)," + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, "user_id", "fkey") @@ -121,7 +122,7 @@ private static int insertUser_Transaction(Start start, Connection con, AppIdenti private static int insertDevice_Transaction(Start start, Connection con, AppIdentifier appIdentifier, TOTPDevice device) throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + Config.getConfig(start).getTotpUserDevicesTable() - + " (app_id, user_id, device_name, secret_key, period, skew, verified) VALUES (?, ?, ?, ?, ?, ?, ?)"; + + " (app_id, user_id, device_name, secret_key, period, skew, verified, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; return update(con, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); @@ -131,6 +132,7 @@ private static int insertDevice_Transaction(Start start, Connection con, AppIden pst.setInt(5, device.period); pst.setInt(6, device.skew); pst.setBoolean(7, device.verified); + pst.setLong(8, device.createdAt); }); } @@ -326,7 +328,8 @@ public TOTPDevice map(ResultSet result) throws SQLException { result.getString("secret_key"), result.getInt("period"), result.getInt("skew"), - result.getBoolean("verified")); + result.getBoolean("verified"), + result.getLong("created_at")); } } diff --git a/src/test/java/io/supertokens/storage/postgresql/test/DeadlockTest.java b/src/test/java/io/supertokens/storage/postgresql/test/DeadlockTest.java index 7851e320..22fe29bb 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/DeadlockTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/DeadlockTest.java @@ -284,7 +284,7 @@ public void testConcurrentDeleteAndUpdate() throws Exception { // Create a device as well as a user: TOTPSQLStorage totpStorage = (TOTPSQLStorage) StorageLayer.getStorage(process.getProcess()); - TOTPDevice device = new TOTPDevice("user", "d1", "secret", 30, 1, false); + TOTPDevice device = new TOTPDevice("user", "d1", "secret", 30, 1, false, System.currentTimeMillis()); totpStorage.createDevice(new AppIdentifier(null, null), device); long now = System.currentTimeMillis(); @@ -446,7 +446,7 @@ public void testConcurrentDeleteAndInsert() throws Exception { // Create a device as well as a user: TOTPSQLStorage totpStorage = (TOTPSQLStorage) StorageLayer.getStorage(process.getProcess()); - TOTPDevice device = new TOTPDevice("user", "d1", "secret", 30, 1, false); + TOTPDevice device = new TOTPDevice("user", "d1", "secret", 30, 1, false, System.currentTimeMillis()); totpStorage.createDevice(new AppIdentifier(null, null), device); long now = System.currentTimeMillis(); diff --git a/src/test/java/io/supertokens/storage/postgresql/test/StorageLayerTest.java b/src/test/java/io/supertokens/storage/postgresql/test/StorageLayerTest.java index efd4ab33..8f6d1699 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/StorageLayerTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/StorageLayerTest.java @@ -85,7 +85,7 @@ public void totpCodeLengthTest() throws Exception { long now = System.currentTimeMillis(); long nextDay = now + 1000 * 60 * 60 * 24; // 1 day from now - TOTPDevice d1 = new TOTPDevice("user", "d1", "secret", 30, 1, false); + TOTPDevice d1 = new TOTPDevice("user", "d1", "secret", 30, 1, false, System.currentTimeMillis()); storage.createDevice(new AppIdentifier(null, null), d1); // Try code with length > 8 From dba89ba4def445fcd1c325ba4f44ecf6fe44115b Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Wed, 1 Nov 2023 17:46:33 +0530 Subject: [PATCH 094/106] fix: mfa stats (#170) * fix: mfa stats * fix: pr comments --- .../supertokens/storage/postgresql/Start.java | 56 ++++--------- .../queries/ActiveUsersQueries.java | 83 +++++++++---------- .../postgresql/queries/GeneralQueries.java | 28 ++++++- 3 files changed, 85 insertions(+), 82 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index ae43d800..7b347d9a 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -1319,44 +1319,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 { @@ -3037,4 +2999,22 @@ public UserIdMapping[] getUserIdMapping_Transaction(TransactionConnection con, A throw new StorageQueryException(e); } } + + @Override + public int getUsersCountWithMoreThanOneLoginMethodOrTOTPEnabled(AppIdentifier appIdentifier) throws StorageQueryException { + try { + return GeneralQueries.getUsersCountWithMoreThanOneLoginMethodOrTOTPEnabled(this, appIdentifier); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public int countUsersThatHaveMoreThanOneLoginMethodOrTOTPEnabledAndActiveSince(AppIdentifier appIdentifier, long sinceTime) throws StorageQueryException { + try { + return ActiveUsersQueries.countUsersThatHaveMoreThanOneLoginMethodOrTOTPEnabledAndActiveSince(this, appIdentifier, sinceTime); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java index 3faeda33..3a39c384 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java @@ -11,6 +11,7 @@ import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.update; +import static io.supertokens.storage.postgresql.config.Config.getConfig; public class ActiveUsersQueries { static String getQueryToCreateUserLastActiveTable(Start start) { @@ -51,9 +52,10 @@ 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() + + " FROM " + Config.getConfig(start).getAppIdToUserIdTable() + " WHERE primary_or_recipe_user_id IN (" + " SELECT user_id FROM " + Config.getConfig(start).getUserLastActiveTable() + " WHERE app_id = ? AND last_active_time >= ?" @@ -71,48 +73,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() @@ -160,4 +120,41 @@ public static void deleteUserActive_Transaction(Connection con, Start start, App pst.setString(2, userId); }); } + + public static int countUsersThatHaveMoreThanOneLoginMethodOrTOTPEnabledAndActiveSince(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 (DISTINCT user_id) as c FROM (" + + " (" // users with more than one login method + + " SELECT primary_or_recipe_user_id AS user_id FROM (" + + " SELECT COUNT(user_id) as num_login_methods, app_id, primary_or_recipe_user_id" + + " FROM " + getConfig(start).getAppIdToUserIdTable() + + " WHERE app_id = ? AND primary_or_recipe_user_id IN (" + + " SELECT user_id FROM " + getConfig(start).getUserLastActiveTable() + + " WHERE app_id = ? AND last_active_time >= ?" + + " )" + + " GROUP BY (app_id, primary_or_recipe_user_id)" + + " ) AS nloginmethods" + + " WHERE num_login_methods > 1" + + " ) UNION (" // TOTP users + + " SELECT user_id FROM " + getConfig(start).getTotpUsersTable() + + " WHERE app_id = ? AND user_id IN (" + + " SELECT user_id FROM " + getConfig(start).getUserLastActiveTable() + + " WHERE app_id = ? AND last_active_time >= ?" + + " )" + + " )" + + ") AS all_users"; + + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, appIdentifier.getAppId()); + pst.setLong(3, sinceTime); + pst.setString(4, appIdentifier.getAppId()); + pst.setString(5, appIdentifier.getAppId()); + pst.setLong(6, sinceTime); + }, result -> { + return result.next() ? result.getInt("c") : 0; + }); + } } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java index 0c1bbc07..4d8af52b 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java @@ -1711,7 +1711,7 @@ public static int getUsersCountWithMoreThanOneLoginMethod(Start start, AppIdenti throws SQLException, StorageQueryException { String QUERY = "SELECT COUNT (1) as c FROM (" + " SELECT COUNT(user_id) as num_login_methods " - + " FROM " + getConfig(start).getUsersTable() + + " FROM " + getConfig(start).getAppIdToUserIdTable() + " WHERE app_id = ? " + " GROUP BY (app_id, primary_or_recipe_user_id) " + ") as nloginmethods WHERE num_login_methods > 1"; @@ -1723,6 +1723,32 @@ public static int getUsersCountWithMoreThanOneLoginMethod(Start start, AppIdenti }); } + public static int getUsersCountWithMoreThanOneLoginMethodOrTOTPEnabled(Start start, AppIdentifier appIdentifier) + throws SQLException, StorageQueryException { + String QUERY = + "SELECT COUNT (DISTINCT user_id) as c FROM (" + + " (" // Users with number of login methods > 1 + + " SELECT primary_or_recipe_user_id AS user_id FROM (" + + " SELECT COUNT(user_id) as num_login_methods, app_id, primary_or_recipe_user_id" + + " FROM " + getConfig(start).getAppIdToUserIdTable() + + " WHERE app_id = ? " + + " GROUP BY (app_id, primary_or_recipe_user_id)" + + " ) AS nloginmethods" + + " WHERE num_login_methods > 1" + + " ) UNION (" // TOTP users + + " SELECT user_id FROM " + getConfig(start).getTotpUsersTable() + + " WHERE app_id = ?" + + " )" + + ") AS all_users"; + + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, appIdentifier.getAppId()); + }, result -> { + return result.next() ? result.getInt("c") : 0; + }); + } + public static boolean checkIfUsesAccountLinking(Start start, AppIdentifier appIdentifier) throws SQLException, StorageQueryException { String QUERY = "SELECT 1 FROM " From 72589b2c1304e2aa87f24a54d7ba22f67e0709ca Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Mon, 6 Nov 2023 14:14:35 +0530 Subject: [PATCH 095/106] fix: index name --- .../storage/postgresql/queries/MultitenancyQueries.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/MultitenancyQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/MultitenancyQueries.java index 5a0c8065..a2ff0ebc 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/MultitenancyQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/MultitenancyQueries.java @@ -172,7 +172,7 @@ public static String getQueryToCreateTenantIdIndexForDefaultRequiredFactorIdsTab } public static String getQueryToCreateOrderIndexForDefaultRequiredFactorIdsTable(Start start) { - return "CREATE INDEX IF NOT EXISTS tenant_default_required_factor_ids_tenant_id_index ON " + return "CREATE INDEX IF NOT EXISTS tenant_default_required_factor_ids_order_idx_index ON " + getConfig(start).getTenantDefaultRequiredFactorIdsTable() + " (order_idx ASC);"; } From 08043dc87569531c0756253b6bf6fa4b40254f5d Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Mon, 25 Dec 2023 14:35:34 +0530 Subject: [PATCH 096/106] fix: mfa changes (#177) --- .../postgresql/config/PostgreSQLConfig.java | 4 +- .../postgresql/queries/GeneralQueries.java | 11 ++--- .../queries/MultitenancyQueries.java | 28 ++++--------- .../queries/multitenancy/MfaSqlHelper.java | 18 ++++---- .../multitenancy/TenantConfigSQLHelper.java | 32 ++++++-------- .../postgresql/test/AccountLinkingTests.java | 4 +- .../storage/postgresql/test/LoggingTest.java | 2 +- .../test/SuperTokensSaaSSecretTest.java | 6 +-- .../test/multitenancy/StorageLayerTest.java | 42 +++++++++---------- .../TestUserPoolIdChangeBehaviour.java | 8 ++-- 10 files changed, 66 insertions(+), 89 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java index 27023cdf..c8e7a0cb 100644 --- a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java +++ b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java @@ -186,8 +186,8 @@ public String getTenantFirstFactorsTable() { return addSchemaAndPrefixToTableName("tenant_first_factors"); } - public String getTenantDefaultRequiredFactorIdsTable() { - return addSchemaAndPrefixToTableName("tenant_default_required_factor_ids"); + public String getTenantRequiredSecondaryFactorsTable() { + return addSchemaAndPrefixToTableName("tenant_required_secondary_factors"); } public String getTenantThirdPartyProvidersTable() { diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java index 07a40c44..8bc2d561 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java @@ -336,16 +336,13 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getTenantDefaultRequiredFactorIdsTable())) { + if (!doesTableExists(start, Config.getConfig(start).getTenantRequiredSecondaryFactorsTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, MultitenancyQueries.getQueryToCreateDefaultRequiredFactorIdsTable(start), NO_OP_SETTER); + update(start, MultitenancyQueries.getQueryToCreateRequiredSecondaryFactorsTable(start), NO_OP_SETTER); // index update(start, - MultitenancyQueries.getQueryToCreateTenantIdIndexForDefaultRequiredFactorIdsTable(start), - NO_OP_SETTER); - update(start, - MultitenancyQueries.getQueryToCreateOrderIndexForDefaultRequiredFactorIdsTable(start), + MultitenancyQueries.getQueryToCreateTenantIdIndexForRequiredSecondaryFactorsTable(start), NO_OP_SETTER); } @@ -591,7 +588,7 @@ public static void deleteAllTables(Start start) throws SQLException, StorageQuer + getConfig(start).getUsersTable() + "," + getConfig(start).getAccessTokenSigningKeysTable() + "," + getConfig(start).getTenantFirstFactorsTable() + "," - + getConfig(start).getTenantDefaultRequiredFactorIdsTable() + "," + + getConfig(start).getTenantRequiredSecondaryFactorsTable() + "," + getConfig(start).getTenantConfigsTable() + "," + getConfig(start).getTenantThirdPartyProvidersTable() + "," + getConfig(start).getTenantThirdPartyProviderClientsTable() + "," diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/MultitenancyQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/MultitenancyQueries.java index a2ff0ebc..e4801d45 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/MultitenancyQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/MultitenancyQueries.java @@ -49,9 +49,6 @@ static String getQueryToCreateTenantConfigsTable(Start start) { + "email_password_enabled BOOLEAN," + "passwordless_enabled BOOLEAN," + "third_party_enabled BOOLEAN," - + "totp_enabled BOOLEAN," - + "has_first_factors BOOLEAN DEFAULT FALSE," - + "has_default_required_factor_ids BOOLEAN DEFAULT FALSE," + "CONSTRAINT " + Utils.getConstraintName(schema, tenantConfigsTable, null, "pkey") + " PRIMARY KEY (connection_uri_domain, app_id, tenant_id)" + ");"; // @formatter:on @@ -145,36 +142,29 @@ public static String getQueryToCreateTenantIdIndexForFirstFactorsTable(Start sta + getConfig(start).getTenantFirstFactorsTable() + " (connection_uri_domain, app_id, tenant_id);"; } - public static String getQueryToCreateDefaultRequiredFactorIdsTable(Start start) { + public static String getQueryToCreateRequiredSecondaryFactorsTable(Start start) { String schema = Config.getConfig(start).getTableSchema(); - String tableName = Config.getConfig(start).getTenantDefaultRequiredFactorIdsTable(); + String tableName = Config.getConfig(start).getTenantRequiredSecondaryFactorsTable(); // @formatter:off return "CREATE TABLE IF NOT EXISTS " + tableName + " (" + "connection_uri_domain VARCHAR(256) DEFAULT ''," + "app_id VARCHAR(64) DEFAULT 'public'," + "tenant_id VARCHAR(64) DEFAULT 'public'," + "factor_id VARCHAR(128)," - + "order_idx INTEGER NOT NULL," + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, null, "pkey") + " PRIMARY KEY (connection_uri_domain, app_id, tenant_id, factor_id)," + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, "tenant_id", "fkey") + " FOREIGN KEY (connection_uri_domain, app_id, tenant_id)" - + " REFERENCES " + Config.getConfig(start).getTenantConfigsTable() + " (connection_uri_domain, app_id, tenant_id) ON DELETE CASCADE," - + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, "order_idx", "key") - + " UNIQUE (connection_uri_domain, app_id, tenant_id, order_idx)" + + " REFERENCES " + Config.getConfig(start).getTenantConfigsTable() + " (connection_uri_domain, app_id, tenant_id) ON DELETE CASCADE" + ");"; // @formatter:on } - public static String getQueryToCreateTenantIdIndexForDefaultRequiredFactorIdsTable(Start start) { + public static String getQueryToCreateTenantIdIndexForRequiredSecondaryFactorsTable(Start start) { return "CREATE INDEX IF NOT EXISTS tenant_default_required_factor_ids_tenant_id_index ON " - + getConfig(start).getTenantDefaultRequiredFactorIdsTable() + " (connection_uri_domain, app_id, tenant_id);"; + + getConfig(start).getTenantRequiredSecondaryFactorsTable() + " (connection_uri_domain, app_id, tenant_id);"; } - public static String getQueryToCreateOrderIndexForDefaultRequiredFactorIdsTable(Start start) { - return "CREATE INDEX IF NOT EXISTS tenant_default_required_factor_ids_order_idx_index ON " - + getConfig(start).getTenantDefaultRequiredFactorIdsTable() + " (order_idx ASC);"; - } private static void executeCreateTenantQueries(Start start, Connection sqlCon, TenantConfig tenantConfig) throws SQLException, StorageQueryException { @@ -190,7 +180,7 @@ private static void executeCreateTenantQueries(Start start, Connection sqlCon, T } MfaSqlHelper.createFirstFactors(start, sqlCon, tenantConfig.tenantIdentifier, tenantConfig.firstFactors); - MfaSqlHelper.createDefaultRequiredFactorIds(start, sqlCon, tenantConfig.tenantIdentifier, tenantConfig.defaultRequiredFactorIds); + MfaSqlHelper.createRequiredSecondaryFactors(start, sqlCon, tenantConfig.tenantIdentifier, tenantConfig.requiredSecondaryFactors); } public static void createTenantConfig(Start start, TenantConfig tenantConfig) throws StorageQueryException, StorageTransactionLogicException { @@ -272,10 +262,10 @@ public static TenantConfig[] getAllTenants(Start start) throws StorageQueryExcep // Map (tenantIdentifier) -> firstFactors HashMap firstFactorsMap = MfaSqlHelper.selectAllFirstFactors(start); - // Map (tenantIdentifier) -> defaultRequiredFactorIds - HashMap defaultRequiredFactorIdsMap = MfaSqlHelper.selectAllDefaultRequiredFactorIds(start); + // Map (tenantIdentifier) -> requiredSecondaryFactors + HashMap requiredSecondaryFactorsMap = MfaSqlHelper.selectAllRequiredSecondaryFactors(start); - return TenantConfigSQLHelper.selectAll(start, providerMap, firstFactorsMap, defaultRequiredFactorIdsMap); + return TenantConfigSQLHelper.selectAll(start, providerMap, firstFactorsMap, requiredSecondaryFactorsMap); } catch (SQLException throwables) { throw new StorageQueryException(throwables); } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/MfaSqlHelper.java b/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/MfaSqlHelper.java index 2157c248..b5abf91d 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/MfaSqlHelper.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/MfaSqlHelper.java @@ -54,10 +54,10 @@ public static HashMap selectAllFirstFactors(Start st }); } - public static HashMap selectAllDefaultRequiredFactorIds(Start start) + public static HashMap selectAllRequiredSecondaryFactors(Start start) throws SQLException, StorageQueryException { - String QUERY = "SELECT connection_uri_domain, app_id, tenant_id, factor_id, order_idx FROM " - + getConfig(start).getTenantDefaultRequiredFactorIdsTable() + " ORDER BY order_idx ASC;"; + String QUERY = "SELECT connection_uri_domain, app_id, tenant_id, factor_id FROM " + + getConfig(start).getTenantRequiredSecondaryFactorsTable() + ";"; return execute(start, QUERY, pst -> {}, result -> { HashMap> defaultRequiredFactors = new HashMap<>(); @@ -97,24 +97,20 @@ public static void createFirstFactors(Start start, Connection sqlCon, TenantIden } } - public static void createDefaultRequiredFactorIds(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String[] defaultRequiredFactorIds) + public static void createRequiredSecondaryFactors(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String[] requiredSecondaryFactors) throws SQLException, StorageQueryException { - if (defaultRequiredFactorIds == null || defaultRequiredFactorIds.length == 0) { + if (requiredSecondaryFactors == null || requiredSecondaryFactors.length == 0) { return; } - String QUERY = "INSERT INTO " + getConfig(start).getTenantDefaultRequiredFactorIdsTable() + "(connection_uri_domain, app_id, tenant_id, factor_id, order_idx) VALUES (?, ?, ?, ?, ?);"; - int orderIdx = 0; - for (String factorId : defaultRequiredFactorIds) { - int finalOrderIdx = orderIdx; + String QUERY = "INSERT INTO " + getConfig(start).getTenantRequiredSecondaryFactorsTable() + "(connection_uri_domain, app_id, tenant_id, factor_id) VALUES (?, ?, ?, ?);"; + for (String factorId : requiredSecondaryFactors) { update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getConnectionUriDomain()); pst.setString(2, tenantIdentifier.getAppId()); pst.setString(3, tenantIdentifier.getTenantId()); pst.setString(4, factorId); - pst.setInt(5, finalOrderIdx); }); - orderIdx++; } } } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/TenantConfigSQLHelper.java b/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/TenantConfigSQLHelper.java index 7d37d531..1a2e00b2 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/TenantConfigSQLHelper.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/TenantConfigSQLHelper.java @@ -41,16 +41,16 @@ public class TenantConfigSQLHelper { public static class TenantConfigRowMapper implements RowMapper { ThirdPartyConfig.Provider[] providers; String[] firstFactors; - String[] defaultRequiredFactorIds; + String[] requiredSecondaryFactors; - private TenantConfigRowMapper(ThirdPartyConfig.Provider[] providers, String[] firstFactors, String[] defaultRequiredFactorIds) { + private TenantConfigRowMapper(ThirdPartyConfig.Provider[] providers, String[] firstFactors, String[] requiredSecondaryFactors) { this.providers = providers; this.firstFactors = firstFactors; - this.defaultRequiredFactorIds = defaultRequiredFactorIds; + this.requiredSecondaryFactors = requiredSecondaryFactors; } - public static TenantConfigSQLHelper.TenantConfigRowMapper getInstance(ThirdPartyConfig.Provider[] providers, String[] firstFactors, String[] defaultRequiredFactorIds) { - return new TenantConfigSQLHelper.TenantConfigRowMapper(providers, firstFactors, defaultRequiredFactorIds); + public static TenantConfigSQLHelper.TenantConfigRowMapper getInstance(ThirdPartyConfig.Provider[] providers, String[] firstFactors, String[] requiredSecondaryFactors) { + return new TenantConfigSQLHelper.TenantConfigRowMapper(providers, firstFactors, requiredSecondaryFactors); } @Override @@ -61,9 +61,8 @@ public TenantConfig map(ResultSet result) throws StorageQueryException { new EmailPasswordConfig(result.getBoolean("email_password_enabled")), new ThirdPartyConfig(result.getBoolean("third_party_enabled"), this.providers), new PasswordlessConfig(result.getBoolean("passwordless_enabled")), - new TotpConfig(result.getBoolean("totp_enabled")), - result.getBoolean("has_first_factors") ? firstFactors : null, - result.getBoolean("has_default_required_factor_ids") ? defaultRequiredFactorIds : null, + firstFactors.length == 0 ? null : firstFactors, + requiredSecondaryFactors.length == 0 ? null : requiredSecondaryFactors, JsonUtils.stringToJsonObject(result.getString("core_config")) ); } catch (Exception e) { @@ -72,11 +71,10 @@ public TenantConfig map(ResultSet result) throws StorageQueryException { } } - public static TenantConfig[] selectAll(Start start, HashMap> providerMap, HashMap firstFactorsMap, HashMap defaultRequiredFactorIdsMap) + public static TenantConfig[] selectAll(Start start, HashMap> providerMap, HashMap firstFactorsMap, HashMap requiredSecondaryFactorsMap) throws SQLException, StorageQueryException { String QUERY = "SELECT connection_uri_domain, app_id, tenant_id, core_config," - + " email_password_enabled, passwordless_enabled, third_party_enabled," - + " totp_enabled, has_first_factors, has_default_required_factor_ids FROM " + + " email_password_enabled, passwordless_enabled, third_party_enabled FROM " + getConfig(start).getTenantConfigsTable() + ";"; TenantConfig[] tenantConfigs = execute(start, QUERY, pst -> {}, result -> { @@ -89,9 +87,9 @@ public static TenantConfig[] selectAll(Start start, HashMap { pst.setString(1, tenantConfig.tenantIdentifier.getConnectionUriDomain()); @@ -118,9 +115,6 @@ public static void create(Start start, Connection sqlCon, TenantConfig tenantCon pst.setBoolean(5, tenantConfig.emailPasswordConfig.enabled); pst.setBoolean(6, tenantConfig.passwordlessConfig.enabled); pst.setBoolean(7, tenantConfig.thirdPartyConfig.enabled); - pst.setBoolean(8, tenantConfig.totpConfig.enabled); - pst.setBoolean(9, tenantConfig.firstFactors != null); - pst.setBoolean(10, tenantConfig.defaultRequiredFactorIds != null); }); } diff --git a/src/test/java/io/supertokens/storage/postgresql/test/AccountLinkingTests.java b/src/test/java/io/supertokens/storage/postgresql/test/AccountLinkingTests.java index 64d98e22..73b4728a 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/AccountLinkingTests.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/AccountLinkingTests.java @@ -88,7 +88,7 @@ public void canLinkFailsIfTryingToLinkUsersAcrossDifferentStorageLayers() throws new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - new TotpConfig(false), null, null, + null, null, coreConfig ) ); @@ -131,7 +131,7 @@ public void canLinkFailsIfTryingToLinkUsersAcrossDifferentStorageLayers() throws new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - new TotpConfig(false), null, null, + null, null, coreConfig ) ); diff --git a/src/test/java/io/supertokens/storage/postgresql/test/LoggingTest.java b/src/test/java/io/supertokens/storage/postgresql/test/LoggingTest.java index b9c65712..51651b9a 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/LoggingTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/LoggingTest.java @@ -284,7 +284,7 @@ public void confirmHikariLoggerClosedOnlyWhenProcessEnds() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - new TotpConfig(false), null, null, + null, null, config ), false); diff --git a/src/test/java/io/supertokens/storage/postgresql/test/SuperTokensSaaSSecretTest.java b/src/test/java/io/supertokens/storage/postgresql/test/SuperTokensSaaSSecretTest.java index c6c7a376..7c103e77 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/SuperTokensSaaSSecretTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/SuperTokensSaaSSecretTest.java @@ -89,7 +89,7 @@ public void testThatTenantCannotSetDatabaseRelatedConfigIfSuperTokensSaaSSecretI Multitenancy.addNewOrUpdateAppOrTenant(process.main, new TenantConfig(new TenantIdentifier(null, null, "t1"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - new TotpConfig(false), null, null, + null, null, j), true); fail(); } catch (BadPermissionException e) { @@ -166,7 +166,7 @@ public void testThatTenantCanSetDatabaseRelatedConfigIfSuperTokensSaaSSecretIsNo new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - new TotpConfig(false), null, null, + null, null, j), false); } @@ -219,7 +219,7 @@ public void testThatTenantCannotGetDatabaseRelatedConfigIfSuperTokensSaaSSecretI new TenantConfig(new TenantIdentifier(null, null, "t" + i), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - new TotpConfig(false), null, null, + null, null, j)); { diff --git a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java index 572ea0cd..1b967b85 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java @@ -112,7 +112,7 @@ public void mergingTenantWithBaseConfigWorks() new TenantConfig(new TenantIdentifier("abc", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - new TotpConfig(false), null, null, + null, null, tenantConfig)}; Config.loadAllTenantConfig(process.getProcess(), tenants); @@ -163,7 +163,7 @@ public void storageInstanceIsReusedAcrossTenants() new TenantConfig(new TenantIdentifier(null, "abc", null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - new TotpConfig(false), null, null, + null, null, tenantConfig)}; Config.loadAllTenantConfig(process.getProcess(), tenants); @@ -211,17 +211,17 @@ public void storageInstanceIsReusedAcrossTenantsComplex() new TenantConfig(new TenantIdentifier(null, "abc", null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - new TotpConfig(false), null, null, + null, null, tenantConfig), new TenantConfig(new TenantIdentifier(null, "abc", "t1"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - new TotpConfig(false), null, null, + null, null, tenantConfig1), new TenantConfig(new TenantIdentifier(null, null, "t2"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - new TotpConfig(false), null, null, + null, null, tenantConfig1)}; Config.loadAllTenantConfig(process.getProcess(), tenants); @@ -286,7 +286,7 @@ public void mergingTenantWithBaseConfigWithInvalidConfigThrowsErrorWorks() new TenantConfig(new TenantIdentifier("abc", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - new TotpConfig(false), null, null, + null, null, tenantConfig)}; Config.loadAllTenantConfig(process.getProcess(), tenants); @@ -320,7 +320,7 @@ public void mergingTenantWithBaseConfigWithConflictingConfigsThrowsError() new TenantConfig(new TenantIdentifier(null, "abc", null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - new TotpConfig(false), null, null, + null, null, tenantConfig)}; Config.loadAllTenantConfig(process.getProcess(), tenants); @@ -355,7 +355,7 @@ public void mergingDifferentConnectionPoolIdTenantWithBaseConfigWithConflictingC new TenantConfig(new TenantIdentifier(null, "abc", null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - new TotpConfig(false), null, null, + null, null, tenantConfig)}; Config.loadAllTenantConfig(process.getProcess(), tenants); @@ -391,7 +391,7 @@ public void mergingDifferentUserPoolIdTenantWithBaseConfigWithConflictingConfigs new TenantConfig(new TenantIdentifier("abc", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - new TotpConfig(false), null, null, + null, null, tenantConfig)}; Config.loadAllTenantConfig(process.getProcess(), tenants); @@ -442,7 +442,7 @@ public void newStorageIsNotCreatedWhenSameTenantIsAdded() new TenantConfig(new TenantIdentifier(null, "abc", null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - new TotpConfig(false), null, null, + null, null, tenantConfig)}; Config.loadAllTenantConfig(process.getProcess(), tenants); @@ -490,7 +490,7 @@ public void testDifferentWaysToGetConfigBasedOnConnectionURIAndTenantId() tenants[0] = new TenantConfig(new TenantIdentifier("c1", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - new TotpConfig(false), null, null, + null, null, tenantConfig); } @@ -502,7 +502,7 @@ public void testDifferentWaysToGetConfigBasedOnConnectionURIAndTenantId() tenants[1] = new TenantConfig(new TenantIdentifier("c1", null, "t1"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - new TotpConfig(false), null, null, + null, null, tenantConfig); } @@ -512,7 +512,7 @@ public void testDifferentWaysToGetConfigBasedOnConnectionURIAndTenantId() tenants[2] = new TenantConfig(new TenantIdentifier(null, null, "t2"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - new TotpConfig(false), null, null, + null, null, tenantConfig); } @@ -522,7 +522,7 @@ public void testDifferentWaysToGetConfigBasedOnConnectionURIAndTenantId() tenants[3] = new TenantConfig(new TenantIdentifier(null, null, "t1"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - new TotpConfig(false), null, null, + null, null, tenantConfig); } @@ -586,7 +586,7 @@ public void differentUserPoolCreatedBasedOnSchemaInConnectionUri() new TenantConfig(new TenantIdentifier("abc", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - new TotpConfig(false), null, null, + null, null, tenantConfig)}; Config.loadAllTenantConfig(process.getProcess(), tenants); @@ -629,7 +629,7 @@ public void multipleTenantsSameUserPoolAndConnectionPoolShouldWork() new TenantConfig(new TenantIdentifier(null, "abc", null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - new TotpConfig(false), null, null, + null, null, tenantConfig)}; Config.loadAllTenantConfig(process.getProcess(), tenants); @@ -665,7 +665,7 @@ public void multipleTenantsSameUserPoolAndDifferentConnectionPoolShouldWork() new TenantConfig(new TenantIdentifier(null, "abc", null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - new TotpConfig(false), null, null, + null, null, tenantConfig)}; Config.loadAllTenantConfig(process.getProcess(), tenants); @@ -705,7 +705,7 @@ public void testCreating50StorageLayersUsage() new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - new TotpConfig(false), null, null, + null, null, config); try { Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantIdentifier(null, null, null), @@ -759,7 +759,7 @@ public void testCantCreateTenantWithUnknownDb() new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - new TotpConfig(false), null, null, + null, null, tenantConfigJson); try { @@ -801,7 +801,7 @@ public void testTenantCreationAndThenDbDownDbThrowsErrorInRecipesAndDoesntAffect new EmailPasswordConfig(true), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - new TotpConfig(false), null, null, + null, null, tenantConfigJson); StorageLayer.getMultitenancyStorage(process.getProcess()).createTenant(tenantConfig); @@ -882,7 +882,7 @@ public void testBadPortWithNewTenantShouldNotCauseItToWaitInput() throws Excepti new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - new TotpConfig(false), null, null, + null, null, tenantConfigJson); try { diff --git a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/TestUserPoolIdChangeBehaviour.java b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/TestUserPoolIdChangeBehaviour.java index 43811fce..51284930 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/TestUserPoolIdChangeBehaviour.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/TestUserPoolIdChangeBehaviour.java @@ -83,7 +83,7 @@ public void testUsersWorkAfterUserPoolIdChanges() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - new TotpConfig(false), null, null, + null, null, coreConfig ), false); @@ -101,7 +101,7 @@ public void testUsersWorkAfterUserPoolIdChanges() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - new TotpConfig(false), null, null, + null, null, coreConfig ), false); @@ -129,7 +129,7 @@ public void testUsersWorkAfterUserPoolIdChangesAndServerRestart() throws Excepti new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - new TotpConfig(false), null, null, + null, null, coreConfig ), false); @@ -147,7 +147,7 @@ public void testUsersWorkAfterUserPoolIdChangesAndServerRestart() throws Excepti new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - new TotpConfig(false), null, null, + null, null, coreConfig ), false); From 87e64f2ae715ed559aafed9e1b79d249ddd05c7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mih=C3=A1ly=20Lengyel?= Date: Mon, 29 Jan 2024 06:34:37 +0100 Subject: [PATCH 097/106] feat: make refresh update the signing key type of sessions (#180) --- CHANGELOG.md | 3 +++ .../java/io/supertokens/storage/postgresql/Start.java | 4 ++-- .../storage/postgresql/queries/SessionQueries.java | 11 ++++++----- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6d1aeb0..595af847 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Replace `TotpNotEnabledError` with `UnknownUserIdTotpError`. - Support for MFA recipe +- Adds a new `useStaticKey` param to `updateSessionInfo_Transaction` + - This enables smooth switching between `useDynamicAccessTokenSigningKey` settings by allowing refresh calls to + change the signing key type of a session ## [5.0.6] - 2023-12-05 diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 24f9c714..059f014f 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -644,11 +644,11 @@ public SessionInfo getSessionInfo_Transaction(TenantIdentifier tenantIdentifier, @Override public void updateSessionInfo_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, String sessionHandle, String refreshTokenHash2, - long expiry) throws StorageQueryException { + long expiry, boolean useStaticKey) throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { SessionQueries.updateSessionInfo_Transaction(this, sqlCon, tenantIdentifier, sessionHandle, - refreshTokenHash2, expiry); + refreshTokenHash2, expiry, useStaticKey); } catch (SQLException e) { throw new StorageQueryException(e); } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/SessionQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/SessionQueries.java index d6685638..0fe56e4d 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/SessionQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/SessionQueries.java @@ -166,18 +166,19 @@ public static SessionInfo getSessionInfo_Transaction(Start start, Connection con public static void updateSessionInfo_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, String sessionHandle, - String refreshTokenHash2, long expiry) + String refreshTokenHash2, long expiry, boolean useStaticKey) throws SQLException, StorageQueryException { String QUERY = "UPDATE " + getConfig(start).getSessionInfoTable() - + " SET refresh_token_hash_2 = ?, expires_at = ?" + + " SET refresh_token_hash_2 = ?, expires_at = ?, use_static_key = ?" + " WHERE app_id = ? AND tenant_id = ? AND session_handle = ?"; update(con, QUERY, pst -> { pst.setString(1, refreshTokenHash2); pst.setLong(2, expiry); - pst.setString(3, tenantIdentifier.getAppId()); - pst.setString(4, tenantIdentifier.getTenantId()); - pst.setString(5, sessionHandle); + pst.setBoolean(3, useStaticKey); + pst.setString(4, tenantIdentifier.getAppId()); + pst.setString(5, tenantIdentifier.getTenantId()); + pst.setString(6, sessionHandle); }); } From 15a43513ded2796308e6d5ca0570af3aff601d72 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Tue, 27 Feb 2024 13:44:12 +0530 Subject: [PATCH 098/106] fix: Merge latest (#199) * fix: remove db password from logs (#181) * fix: remove db password from logs * fix: Update version * fix: mask db password * fix: Add tests * fix: Add more tests * fix: PR changes * fix: PR changes * fix: Connection pool issue (#182) * fix: test connection pool * fix: changelog * fix: test for downtime during connection pool change * fix: assert that there should be down time * fix: cleanup * adding dev-v5.0.7 tag to this commit to ensure building * fix: cicd tests (#185) * fix: logging test (#187) * adding dev-v5.0.7 tag to this commit to ensure building * fix: flaky test (#188) * adding dev-v5.0.7 tag to this commit to ensure building * fix: adds idle timeout and minimum idle configs (#184) * fix: adds idle timeout and minimum idle configs * fix: protected props * fix: changelog * fix: test protected config * adding dev-v5.0.7 tag to this commit to ensure building * fix: cicd (#189) * fix: cicd * fix: test * adding dev-v5.0.7 tag to this commit to ensure building * fixes tests * adding dev-v5.0.7 tag to this commit to ensure building * fix: vulnerability fix (#192) * fix: vulnerability fix * fix: vulnerability fix * adding dev-v5.0.8 tag to this commit to ensure building * fix: dependencies (#195) * adding dev-v5.0.8 tag to this commit to ensure building * fix: version update (#198) * adding dev-v5.0.8 tag to this commit to ensure building --------- Co-authored-by: Ankit Tiwari Co-authored-by: rishabhpoddar --- CHANGELOG.md | 10 + build.gradle | 18 +- config.yaml | 8 + devConfig.yaml | 8 + implementationDependencies.json | 12 +- ...-5.0.6.jar => postgresql-plugin-5.0.8.jar} | Bin 211671 -> 213545 bytes .../storage/postgresql/ConnectionPool.java | 4 + .../supertokens/storage/postgresql/Start.java | 21 +- .../postgresql/config/PostgreSQLConfig.java | 43 +- .../postgresql/output/CustomLayout.java | 4 +- .../storage/postgresql/output/Logging.java | 11 +- .../storage/postgresql/utils/Utils.java | 17 + .../postgresql/test/DbConnectionPoolTest.java | 396 ++++++++++++++++++ .../storage/postgresql/test/LoggingTest.java | 274 ++++++++++++ .../test/SuperTokensSaaSSecretTest.java | 6 +- 15 files changed, 802 insertions(+), 30 deletions(-) rename jar/{postgresql-plugin-5.0.6.jar => postgresql-plugin-5.0.8.jar} (74%) create mode 100644 src/test/java/io/supertokens/storage/postgresql/test/DbConnectionPoolTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 595af847..627fa56f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,16 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - This enables smooth switching between `useDynamicAccessTokenSigningKey` settings by allowing refresh calls to change the signing key type of a session +## [5.0.8] - 2024-02-19 + +- Fixes vulnerabilities in dependencies + +## [5.0.7] - 2024-01-25 + +- Fixes the issue where passwords were inadvertently logged in the logs. +- Adds tests to check connection pool behaviour. +- Adds `postgresql_idle_connection_timeout` and `postgresql_minimum_idle_connections` configs to control active connections to the database. + ## [5.0.6] - 2023-12-05 - Validates db config types in `canBeUsed` function diff --git a/build.gradle b/build.gradle index 754f70d7..713fefbe 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'java-library' } -version = "5.0.6" +version = "5.0.8" repositories { mavenCentral() @@ -17,16 +17,16 @@ dependencies { implementation group: 'com.zaxxer', name: 'HikariCP', version: '3.4.1' // https://mvnrepository.com/artifact/org.postgresql/postgresql - implementation group: 'org.postgresql', name: 'postgresql', version: '42.2.10' + implementation group: 'org.postgresql', name: 'postgresql', version: '42.7.2' // https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml - compileOnly group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.14.0' + compileOnly group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.16.1' // https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core - compileOnly group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.14.0' + compileOnly group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.16.1' // https://mvnrepository.com/artifact/ch.qos.logback/logback-classic - compileOnly group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.3' + compileOnly group: 'ch.qos.logback', name: 'logback-classic', version: '1.4.14' // https://mvnrepository.com/artifact/com.google.code.findbugs/jsr305 compileOnly group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2' @@ -43,10 +43,10 @@ dependencies { testImplementation group: 'org.mockito', name: 'mockito-core', version: '3.1.0' // https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-core - testImplementation group: 'org.apache.tomcat.embed', name: 'tomcat-embed-core', version: '10.1.1' + testImplementation group: 'org.apache.tomcat.embed', name: 'tomcat-embed-core', version: '10.1.18' // https://mvnrepository.com/artifact/ch.qos.logback/logback-classic - testImplementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.3' + testImplementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.4.14' // https://mvnrepository.com/artifact/com.google.code.gson/gson testImplementation group: 'com.google.code.gson', name: 'gson', version: '2.3.1' @@ -54,10 +54,10 @@ dependencies { testImplementation 'com.tngtech.archunit:archunit-junit4:0.22.0' // https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml - testImplementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.14.0' + testImplementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.16.1' // https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core - testImplementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.14.0' + testImplementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.16.1' } jar { diff --git a/config.yaml b/config.yaml index 36459b8d..38ade78f 100644 --- a/config.yaml +++ b/config.yaml @@ -67,3 +67,11 @@ postgresql_config_version: 0 # (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "thirdparty_users") string value. Specify the name of the table # that will store the thirdparty recipe users. # postgresql_thirdparty_users_table_name + +# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: 60000) long value. Timeout in milliseconds for the idle connections +# to be closed. +# postgresql_idle_connection_timeout: + +# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: null) integer value. Minimum number of idle connections to be kept +# active. If not set, minimum idle connections will be same as the connection pool size. +# postgresql_minimum_idle_connections: diff --git a/devConfig.yaml b/devConfig.yaml index 39d0d5ed..a25dba97 100644 --- a/devConfig.yaml +++ b/devConfig.yaml @@ -69,3 +69,11 @@ postgresql_password: "root" # (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "thirdparty_users") string value. Specify the name of the table # that will store the thirdparty recipe users. # postgresql_thirdparty_users_table_name + +# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: 60000) long value. Timeout in milliseconds for the idle connections +# to be closed. +# postgresql_idle_connection_timeout: + +# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: null) integer value. Minimum number of idle connections to be kept +# active. If not set, minimum idle connections will be same as the connection pool size. +# postgresql_minimum_idle_connections: diff --git a/implementationDependencies.json b/implementationDependencies.json index 6c885fc4..a3b16e26 100644 --- a/implementationDependencies.json +++ b/implementationDependencies.json @@ -2,9 +2,9 @@ "_comment": "Contains list of implementation dependencies URL for this project", "list": [ { - "jar": "https://repo1.maven.org/maven2/org/postgresql/postgresql/42.2.10/postgresql-42.2.10.jar", - "name": "PostgreSQL JDBC Driver 4.2", - "src": "https://repo1.maven.org/maven2/org/postgresql/postgresql/42.2.10/postgresql-42.2.10-sources.jar" + "jar": "https://repo1.maven.org/maven2/org/postgresql/postgresql/42.7.2/postgresql-42.7.2.jar", + "name": "PostgreSQL JDBC Driver 42.7.2", + "src": "https://repo1.maven.org/maven2/org/postgresql/postgresql/42.7.2/postgresql-42.7.2-sources.jar" }, { "jar": "https://repo1.maven.org/maven2/com/zaxxer/HikariCP/3.4.1/HikariCP-3.4.1.jar", @@ -12,9 +12,9 @@ "src": "https://repo1.maven.org/maven2/com/zaxxer/HikariCP/3.4.1/HikariCP-3.4.1-sources.jar" }, { - "jar": "https://repo1.maven.org/maven2/org/slf4j/slf4j-api/1.7.25/slf4j-api-1.7.25.jar", - "name": "SLF4j API 1.7.25", - "src": "https://repo1.maven.org/maven2/org/slf4j/slf4j-api/1.7.25/slf4j-api-1.7.25-sources.jar" + "jar": "https://repo1.maven.org/maven2/org/slf4j/slf4j-api/2.0.7/slf4j-api-2.0.7.jar", + "name": "SLF4j API 2.0.7", + "src": "https://repo1.maven.org/maven2/org/slf4j/slf4j-api/2.0.7/slf4j-api-2.0.7-sources.jar" } ] } diff --git a/jar/postgresql-plugin-5.0.6.jar b/jar/postgresql-plugin-5.0.8.jar similarity index 74% rename from jar/postgresql-plugin-5.0.6.jar rename to jar/postgresql-plugin-5.0.8.jar index e1cc4cccec93082e06a684117033f53d3444bc35..d1a855bd6fe0aa5d60dd5df094cced1d43cfef0b 100644 GIT binary patch delta 48993 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 delta 46952 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

c0eGrm@N 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 diff --git a/src/main/java/io/supertokens/storage/postgresql/ConnectionPool.java b/src/main/java/io/supertokens/storage/postgresql/ConnectionPool.java index f7dbc287..d0b8c6f0 100644 --- a/src/main/java/io/supertokens/storage/postgresql/ConnectionPool.java +++ b/src/main/java/io/supertokens/storage/postgresql/ConnectionPool.java @@ -81,6 +81,10 @@ private synchronized void initialiseHikariDataSource() throws SQLException { } config.setMaximumPoolSize(userConfig.getConnectionPoolSize()); config.setConnectionTimeout(5000); + if (userConfig.getMinimumIdleConnections() != null) { + config.setMinimumIdle(userConfig.getMinimumIdleConnections()); + config.setIdleTimeout(userConfig.getIdleConnectionTimeout()); + } config.addDataSourceProperty("cachePrepStmts", "true"); config.addDataSourceProperty("prepStmtCacheSize", "250"); config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 059f014f..c5a72782 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -103,6 +103,8 @@ import java.util.List; import java.util.Set; +import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; + public class Start implements SessionSQLStorage, EmailPasswordSQLStorage, EmailVerificationSQLStorage, ThirdPartySQLStorage, JWTRecipeSQLStorage, PasswordlessSQLStorage, UserMetadataSQLStorage, UserRolesSQLStorage, UserIdMappingStorage, @@ -113,7 +115,8 @@ public class Start // SaaS. If the core is not running in SuperTokens SaaS, this array has no effect. private static String[] PROTECTED_DB_CONFIG = new String[]{"postgresql_connection_pool_size", "postgresql_connection_uri", "postgresql_host", "postgresql_port", "postgresql_user", "postgresql_password", - "postgresql_database_name", "postgresql_table_schema"}; + "postgresql_database_name", "postgresql_table_schema", "postgresql_idle_connection_timeout", + "postgresql_minimum_idle_connections"}; private static final Object appenderLock = new Object(); public static boolean silent = false; private ResourceDistributor resourceDistributor = new ResourceDistributor(); @@ -3017,9 +3020,23 @@ public int getUsersCountWithMoreThanOneLoginMethodOrTOTPEnabled(AppIdentifier ap @Override public int countUsersThatHaveMoreThanOneLoginMethodOrTOTPEnabledAndActiveSince(AppIdentifier appIdentifier, long sinceTime) throws StorageQueryException { try { - return ActiveUsersQueries.countUsersThatHaveMoreThanOneLoginMethodOrTOTPEnabledAndActiveSince(this, appIdentifier, sinceTime); + return ActiveUsersQueries.countUsersThatHaveMoreThanOneLoginMethodOrTOTPEnabledAndActiveSince(this, + appIdentifier, sinceTime); } catch (SQLException e) { throw new StorageQueryException(e); } } + + @TestOnly + public int getDbActivityCount(String dbname) throws SQLException, StorageQueryException { + String QUERY = "SELECT COUNT(*) as c FROM pg_stat_activity WHERE datname = ?;"; + return execute(this, QUERY, pst -> { + pst.setString(1, dbname); + }, result -> { + if (result.next()) { + return result.getInt("c"); + } + return -1; + }); + } } diff --git a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java index c8e7a0cb..e0a0c682 100644 --- a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java +++ b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java @@ -112,6 +112,14 @@ public class PostgreSQLConfig { @ConnectionPoolProperty private String postgresql_connection_scheme = "postgresql"; + @JsonProperty + @ConnectionPoolProperty + private long postgresql_idle_connection_timeout = 60000; + + @JsonProperty + @ConnectionPoolProperty + private Integer postgresql_minimum_idle_connections = null; + @IgnoreForAnnotationCheck boolean isValidAndNormalised = false; @@ -242,6 +250,14 @@ public String getThirdPartyUsersTable() { return postgresql_thirdparty_users_table_name; } + public long getIdleConnectionTimeout() { + return postgresql_idle_connection_timeout; + } + + public Integer getMinimumIdleConnections() { + return postgresql_minimum_idle_connections; + } + public String getThirdPartyUserToTenantTable() { return addSchemaAndPrefixToTableName("thirdparty_user_to_tenant"); } @@ -348,6 +364,19 @@ public void validateAndNormalise() throws InvalidConfigException { "'postgresql_connection_pool_size' in the config.yaml file must be > 0"); } + if (postgresql_minimum_idle_connections != null) { + if (postgresql_minimum_idle_connections < 0) { + throw new InvalidConfigException( + "'postgresql_minimum_idle_connections' must be >= 0"); + } + + if (postgresql_minimum_idle_connections > postgresql_connection_pool_size) { + throw new InvalidConfigException( + "'postgresql_minimum_idle_connections' must be less than or equal to " + + "'postgresql_connection_pool_size'"); + } + } + // Normalisation if (postgresql_connection_uri != null) { { // postgresql_connection_attributes @@ -556,10 +585,18 @@ public String getConnectionPoolId() { StringBuilder connectionPoolId = new StringBuilder(); for (Field field : PostgreSQLConfig.class.getDeclaredFields()) { if (field.isAnnotationPresent(ConnectionPoolProperty.class)) { - connectionPoolId.append("|"); try { - if (field.get(this) != null) { - connectionPoolId.append(field.get(this).toString()); + String fieldName = field.getName(); + String fieldValue = field.get(this) != null ? field.get(this).toString() : null; + if(fieldValue == null) { + continue; + } + // To ensure a unique connectionPoolId we include the database password and use the "|db_pass|" identifier. + // This facilitates easy removal of the password from logs when necessary. + if (fieldName.equals("postgresql_password")) { + connectionPoolId.append("|db_pass|" + fieldValue + "|db_pass"); + } else { + connectionPoolId.append("|" + fieldValue); } } catch (IllegalAccessException e) { throw new RuntimeException(e); diff --git a/src/main/java/io/supertokens/storage/postgresql/output/CustomLayout.java b/src/main/java/io/supertokens/storage/postgresql/output/CustomLayout.java index 6b8a57a1..003558e7 100644 --- a/src/main/java/io/supertokens/storage/postgresql/output/CustomLayout.java +++ b/src/main/java/io/supertokens/storage/postgresql/output/CustomLayout.java @@ -20,7 +20,7 @@ import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.CoreConstants; import ch.qos.logback.core.LayoutBase; -import io.supertokens.storage.postgresql.Start; +import io.supertokens.storage.postgresql.utils.Utils; import java.text.DateFormat; import java.text.SimpleDateFormat; @@ -58,7 +58,7 @@ public String doLayout(ILoggingEvent event) { sbuf.append(event.getCallerData()[1]); sbuf.append(" | "); - sbuf.append(event.getFormattedMessage()); + sbuf.append(Utils.maskDBPassword(event.getFormattedMessage())); sbuf.append(CoreConstants.LINE_SEPARATOR); sbuf.append(CoreConstants.LINE_SEPARATOR); diff --git a/src/main/java/io/supertokens/storage/postgresql/output/Logging.java b/src/main/java/io/supertokens/storage/postgresql/output/Logging.java index 19547def..716f888c 100644 --- a/src/main/java/io/supertokens/storage/postgresql/output/Logging.java +++ b/src/main/java/io/supertokens/storage/postgresql/output/Logging.java @@ -37,10 +37,10 @@ public class Logging extends ResourceDistributor.SingletonResource { private Logging(Start start, String infoLogPath, String errorLogPath) { this.infoLogger = infoLogPath.equals("null") - ? createLoggerForConsole(start, "io.supertokens.storage.postgresql.Info") + ? createLoggerForConsole(start, "io.supertokens.storage.postgresql.Info", LOG_LEVEL.INFO) : createLoggerForFile(start, infoLogPath, "io.supertokens.storage.postgresql.Info"); this.errorLogger = errorLogPath.equals("null") - ? createLoggerForConsole(start, "io.supertokens.storage.postgresql.Error") + ? createLoggerForConsole(start, "io.supertokens.storage.postgresql.Error", LOG_LEVEL.ERROR) : createLoggerForFile(start, errorLogPath, "io.supertokens.storage.postgresql.Error"); } @@ -154,12 +154,12 @@ public static void error(Start start, String message, boolean toConsoleAsWell, E private static void systemOut(String msg) { if (!Start.silent) { - System.out.println(msg); + System.out.println(Utils.maskDBPassword(msg)); } } private static void systemErr(String err) { - System.err.println(err); + System.err.println(Utils.maskDBPassword(err)); } public static void stopLogging(Start start) { @@ -198,7 +198,7 @@ private Logger createLoggerForFile(Start start, String file, String name) { return logger; } - private Logger createLoggerForConsole(Start start, String name) { + private Logger createLoggerForConsole(Start start, String name, LOG_LEVEL logLevel) { Logger logger = (Logger) LoggerFactory.getLogger(name); if (logger.iteratorForAppenders().hasNext()) { @@ -210,6 +210,7 @@ private Logger createLoggerForConsole(Start start, String name) { ple.setContext(lc); ple.start(); ConsoleAppender logConsoleAppender = new ConsoleAppender<>(); + logConsoleAppender.setTarget(logLevel == LOG_LEVEL.ERROR ? "System.err" : "System.out"); logConsoleAppender.setEncoder(ple); logConsoleAppender.setContext(lc); logConsoleAppender.start(); diff --git a/src/main/java/io/supertokens/storage/postgresql/utils/Utils.java b/src/main/java/io/supertokens/storage/postgresql/utils/Utils.java index 5f79f42c..c4b78164 100644 --- a/src/main/java/io/supertokens/storage/postgresql/utils/Utils.java +++ b/src/main/java/io/supertokens/storage/postgresql/utils/Utils.java @@ -21,6 +21,8 @@ import java.io.ByteArrayOutputStream; import java.io.PrintStream; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class Utils { public static String exceptionStacktraceToString(Exception e) { @@ -62,4 +64,19 @@ public static String[] getStringArrayFromJsonString(String input) { } return new Gson().fromJson(input, String[].class); } + + public static String maskDBPassword(String log) { + String regex = "(\\|db_pass\\|)(.*?)(\\|db_pass\\|)"; + + Matcher matcher = Pattern.compile(regex).matcher(log); + StringBuffer maskedLog = new StringBuffer(); + + while (matcher.find()) { + String maskedPassword = "*".repeat(8); + matcher.appendReplacement(maskedLog, "|" + maskedPassword + "|"); + } + + matcher.appendTail(maskedLog); + return maskedLog.toString(); + } } diff --git a/src/test/java/io/supertokens/storage/postgresql/test/DbConnectionPoolTest.java b/src/test/java/io/supertokens/storage/postgresql/test/DbConnectionPoolTest.java new file mode 100644 index 00000000..d214d1bc --- /dev/null +++ b/src/test/java/io/supertokens/storage/postgresql/test/DbConnectionPoolTest.java @@ -0,0 +1,396 @@ +/* + * Copyright (c) 2024, 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.storage.postgresql.test; + +import static org.junit.Assert.*; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +import io.supertokens.pluginInterface.multitenancy.*; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import com.google.gson.JsonObject; + +import io.supertokens.ProcessState; +import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.multitenancy.Multitenancy; +import io.supertokens.multitenancy.exception.BadPermissionException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.storage.postgresql.Start; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.thirdparty.ThirdParty; + +public class DbConnectionPoolTest { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + @Test + public void testActiveConnectionsWithTenants() 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)); + + Start start = (Start) StorageLayer.getBaseStorage(process.getProcess()); + assertEquals(10, start.getDbActivityCount("supertokens")); + + JsonObject config = new JsonObject(); + start.modifyConfigToAddANewUserPoolForTesting(config, 1); + + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + new TenantIdentifier(null, null, "t1"), + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + null, null, config + ), false); + + Thread.sleep(1000); // let the new tenant be ready + + assertEquals(10, start.getDbActivityCount("st1")); + + // change connection pool size + config.addProperty("postgresql_connection_pool_size", 20); + + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + new TenantIdentifier(null, null, "t1"), + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + null, null, config + ), false); + + Thread.sleep(2000); // let the new tenant be ready + + assertEquals(20, start.getDbActivityCount("st1")); + + // delete tenant + Multitenancy.deleteTenant(new TenantIdentifier(null, null, "t1"), process.getProcess()); + Thread.sleep(2000); // let the tenant be deleted + + assertEquals(0, start.getDbActivityCount("st1")); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testDownTimeWhenChangingConnectionPoolSize() throws Exception { + String[] args = {"../"}; + + for (int t = 0; t < 5; t++) { + 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)); + + Start start = (Start) StorageLayer.getBaseStorage(process.getProcess()); + assertEquals(10, start.getDbActivityCount("supertokens")); + + JsonObject config = new JsonObject(); + start.modifyConfigToAddANewUserPoolForTesting(config, 1); + config.addProperty("postgresql_connection_pool_size", 300); + AtomicLong firstErrorTime = new AtomicLong(-1); + AtomicLong successAfterErrorTime = new AtomicLong(-1); + AtomicInteger errorCount = new AtomicInteger(0); + + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + new TenantIdentifier(null, null, "t1"), + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + null, null, config + ), false); + + Thread.sleep(5000); // let the new tenant be ready + + assertEquals(300, start.getDbActivityCount("st1")); + + ExecutorService es = Executors.newFixedThreadPool(100); + + for (int i = 0; i < 10000; i++) { + int finalI = i; + es.execute(() -> { + try { + TenantIdentifier t1 = new TenantIdentifier(null, null, "t1"); + TenantIdentifierWithStorage t1WithStorage = t1.withStorage(StorageLayer.getStorage(t1, process.getProcess())); + ThirdParty.signInUp(t1WithStorage, process.getProcess(), "google", "googleid"+ finalI, "user" + + finalI + "@example.com"); + + if (firstErrorTime.get() != -1 && successAfterErrorTime.get() == -1) { + successAfterErrorTime.set(System.currentTimeMillis()); + } + } catch (StorageQueryException e) { + if (e.getMessage().contains("Connection is closed") || e.getMessage().contains("has been closed")) { + if (firstErrorTime.get() == -1) { + firstErrorTime.set(System.currentTimeMillis()); + } + } else { + errorCount.incrementAndGet(); + throw new RuntimeException(e); + } + } catch (EmailChangeNotAllowedException e) { + errorCount.incrementAndGet(); + throw new RuntimeException(e); + } catch (TenantOrAppNotFoundException e) { + errorCount.incrementAndGet(); + throw new RuntimeException(e); + } catch (BadPermissionException e) { + errorCount.incrementAndGet(); + throw new RuntimeException(e); + } catch (IllegalStateException e) { + if (e.getMessage().contains("Please call initPool before getConnection")) { + if (firstErrorTime.get() == -1) { + firstErrorTime.set(System.currentTimeMillis()); + } + } else { + errorCount.incrementAndGet(); + throw e; + } + } + }); + } + + // change connection pool size + config.addProperty("postgresql_connection_pool_size", 200); + + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + new TenantIdentifier(null, null, "t1"), + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + null, null, config + ), false); + + Thread.sleep(3000); // let the new tenant be ready + + es.shutdown(); + es.awaitTermination(2, TimeUnit.MINUTES); + + assertEquals(0, errorCount.get()); + + assertEquals(200, start.getDbActivityCount("st1")); + + // delete tenant + Multitenancy.deleteTenant(new TenantIdentifier(null, null, "t1"), process.getProcess()); + Thread.sleep(3000); // let the tenant be deleted + + assertEquals(0, start.getDbActivityCount("st1")); + + System.out.println(successAfterErrorTime.get() - firstErrorTime.get() + "ms"); + assertTrue(successAfterErrorTime.get() - firstErrorTime.get() < 250); + + if (successAfterErrorTime.get() - firstErrorTime.get() == 0) { + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + continue; // retry + } + + assertTrue(successAfterErrorTime.get() - firstErrorTime.get() > 0); + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + + return; + } + + fail(); // tried 5 times + } + + + @Test + public void testMinimumIdleConnections() 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("postgresql_connection_pool_size", "20"); + Utils.setValueInConfig("postgresql_minimum_idle_connections", "10"); + Utils.setValueInConfig("postgresql_idle_connection_timeout", "30000"); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + Thread.sleep(65000); // let the idle connections time out + + Start start = (Start) StorageLayer.getBaseStorage(process.getProcess()); + assertEquals(10, start.getDbActivityCount("supertokens")); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + } + + @Test + public void testMinimumIdleConnectionForTenants() 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)); + + Start start = (Start) StorageLayer.getBaseStorage(process.getProcess()); + assertEquals(10, start.getDbActivityCount("supertokens")); + + JsonObject config = new JsonObject(); + start.modifyConfigToAddANewUserPoolForTesting(config, 1); + + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + new TenantIdentifier(null, null, "t1"), + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + null, null, config + ), false); + + Thread.sleep(1000); // let the new tenant be ready + + assertEquals(10, start.getDbActivityCount("st1")); + + // change connection pool size + config.addProperty("postgresql_connection_pool_size", 20); + config.addProperty("postgresql_minimum_idle_connections", 5); + + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + new TenantIdentifier(null, null, "t1"), + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + null, null, config + ), false); + + Thread.sleep(2000); // let the new tenant be ready + + assertEquals(5, start.getDbActivityCount("st1")); + + // delete tenant + Multitenancy.deleteTenant(new TenantIdentifier(null, null, "t1"), process.getProcess()); + Thread.sleep(2000); // let the tenant be deleted + + assertEquals(0, start.getDbActivityCount("st1")); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testIdleConnectionTimeout() 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)); + + Start start = (Start) StorageLayer.getBaseStorage(process.getProcess()); + assertEquals(10, start.getDbActivityCount("supertokens")); + + JsonObject config = new JsonObject(); + start.modifyConfigToAddANewUserPoolForTesting(config, 1); + config.addProperty("postgresql_connection_pool_size", 300); + config.addProperty("postgresql_minimum_idle_connections", 5); + config.addProperty("postgresql_idle_connection_timeout", 30000); + + AtomicLong errorCount = new AtomicLong(0); + + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + new TenantIdentifier(null, null, "t1"), + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + null, null, config + ), false); + + Thread.sleep(3000); // let the new tenant be ready + + assertTrue(10 >= start.getDbActivityCount("st1")); + + ExecutorService es = Executors.newFixedThreadPool(150); + + for (int i = 0; i < 10000; i++) { + int finalI = i; + es.execute(() -> { + try { + TenantIdentifier t1 = new TenantIdentifier(null, null, "t1"); + TenantIdentifierWithStorage t1WithStorage = t1.withStorage(StorageLayer.getStorage(t1, process.getProcess())); + ThirdParty.signInUp(t1WithStorage, process.getProcess(), "google", "googleid"+ finalI, "user" + + finalI + "@example.com"); + + } catch (StorageQueryException e) { + errorCount.incrementAndGet(); + throw new RuntimeException(e); + } catch (EmailChangeNotAllowedException e) { + errorCount.incrementAndGet(); + throw new RuntimeException(e); + } catch (TenantOrAppNotFoundException e) { + errorCount.incrementAndGet(); + throw new RuntimeException(e); + } catch (BadPermissionException e) { + errorCount.incrementAndGet(); + throw new RuntimeException(e); + } + }); + } + + es.shutdown(); + es.awaitTermination(2, TimeUnit.MINUTES); + + assertTrue(5 < start.getDbActivityCount("st1")); + + assertEquals(0, errorCount.get()); + + Thread.sleep(65000); // let the idle connections time out + + assertEquals(5, start.getDbActivityCount("st1")); + + // delete tenant + Multitenancy.deleteTenant(new TenantIdentifier(null, null, "t1"), process.getProcess()); + Thread.sleep(3000); // let the tenant be deleted + + assertEquals(0, start.getDbActivityCount("st1")); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } +} \ No newline at end of file diff --git a/src/test/java/io/supertokens/storage/postgresql/test/LoggingTest.java b/src/test/java/io/supertokens/storage/postgresql/test/LoggingTest.java index 51651b9a..a96a91c8 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/LoggingTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/LoggingTest.java @@ -21,6 +21,8 @@ import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.Appender; import com.google.gson.JsonObject; + +import io.supertokens.Main; import io.supertokens.ProcessState; import io.supertokens.config.Config; import io.supertokens.featureflag.EE_FEATURES; @@ -28,6 +30,7 @@ import io.supertokens.multitenancy.Multitenancy; import io.supertokens.pluginInterface.multitenancy.*; import io.supertokens.storage.postgresql.Start; +import io.supertokens.storage.postgresql.config.PostgreSQLConfig; import io.supertokens.storage.postgresql.output.Logging; import io.supertokens.storageLayer.StorageLayer; import org.apache.tomcat.util.http.fileupload.FileUtils; @@ -310,6 +313,277 @@ public void confirmHikariLoggerClosedOnlyWhenProcessEnds() throws Exception { assertFalse(hikariLogger.iteratorForAppenders().hasNext()); } + @Test + public void testDBPasswordMaskingOnDBConnectionFailUsingConnectionUri() throws Exception { + String[] args = { "../" }; + + String dbUser = "db_user"; + String dbPassword = "db_password"; + String dbName = "db_does_not_exist"; + String dbConnectionUri = "postgresql://" + dbUser + ":" + dbPassword + "@localhost:5432/" + dbName; + + Utils.setValueInConfig("postgresql_connection_uri", dbConnectionUri); + Utils.setValueInConfig("error_log_path", "null"); + + ByteArrayOutputStream errorOutput = new ByteArrayOutputStream(); + System.setErr(new PrintStream(errorOutput)); + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + try { + process.startProcess(); + process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.INIT_FAILURE); + + assertTrue(fileContainsString(errorOutput, dbUser)); + assertTrue(fileContainsString(errorOutput, dbName)); + assertTrue(fileContainsString(errorOutput, "********")); + assertFalse(fileContainsString(errorOutput, dbPassword)); + + } finally { + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + System.setErr(new PrintStream(new FileOutputStream(FileDescriptor.err))); + } + } + + @Test + public void testDBPasswordMaskingOnDBConnectionFailUsingCredentials() throws Exception { + String[] args = { "../" }; + + String dbUser = "db_user"; + String dbPassword = "db_password"; + String dbName = "db_does_not_exist"; + + Utils.commentConfigValue("postgresql_connection_uri"); + Utils.setValueInConfig("postgresql_user", dbUser); + Utils.setValueInConfig("postgresql_password", dbPassword); + Utils.setValueInConfig("postgresql_database_name", dbName); + Utils.setValueInConfig("error_log_path", "null"); + + ByteArrayOutputStream errorOutput = new ByteArrayOutputStream(); + System.setErr(new PrintStream(errorOutput)); + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + try { + process.startProcess(); + process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.INIT_FAILURE); + + assertTrue(fileContainsString(errorOutput, dbUser)); + assertTrue(fileContainsString(errorOutput, dbName)); + assertTrue(fileContainsString(errorOutput, "********")); + assertFalse(fileContainsString(errorOutput, dbPassword)); + + } finally { + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + System.setErr(new PrintStream(new FileOutputStream(FileDescriptor.err))); + } + } + + @Test + public void testDBPasswordMasking() throws Exception { + String[] args = { "../" }; + + ByteArrayOutputStream stdOutput = new ByteArrayOutputStream(); + ByteArrayOutputStream errorOutput = new ByteArrayOutputStream(); + + Utils.setValueInConfig("info_log_path", "null"); + Utils.setValueInConfig("error_log_path", "null"); + + System.setOut(new PrintStream(stdOutput)); + System.setErr(new PrintStream(errorOutput)); + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + + try { + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + Logging.info((Start) StorageLayer.getStorage(process.getProcess()), "INFO LOG: |db_pass|password|db_pass|", + false); + Logging.error((Start) StorageLayer.getStorage(process.getProcess()), + "ERROR LOG: |db_pass|password|db_pass|", false); + + assertTrue(fileContainsString(stdOutput, "INFO LOG: |********|")); + assertTrue(fileContainsString(errorOutput, "ERROR LOG: |********|")); + + } finally { + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + System.setOut(new PrintStream(new FileOutputStream(FileDescriptor.out))); + System.setErr(new PrintStream(new FileOutputStream(FileDescriptor.err))); + } + } + + @Test + public void testDBPasswordIsNotLoggedWhenProcessStartsEnds() throws Exception { + String[] args = { "../" }; + + Utils.setValueInConfig("error_log_path", "null"); + Utils.setValueInConfig("info_log_path", "null"); + + ByteArrayOutputStream stdOutput = new ByteArrayOutputStream(); + ByteArrayOutputStream errorOutput = new ByteArrayOutputStream(); + + System.setOut(new PrintStream(stdOutput)); + System.setErr(new PrintStream(errorOutput)); + + try { + // Case 1: DB Password shouldn't be logged after starting/stopping the process with correct credentials + { + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + Start start = (Start) StorageLayer.getStorage(process.getProcess()); + PostgreSQLConfig userConfig = io.supertokens.storage.postgresql.config.Config.getConfig(start); + String dbPasswordFromConfig = userConfig.getPassword(); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + + assertFalse(fileContainsString(stdOutput, dbPasswordFromConfig)); + assertFalse(fileContainsString(errorOutput, dbPasswordFromConfig)); + } + + // Case 2: DB Password shouldn't be logged after starting/stopping the process with incorrect credentials + { + String dbUser = "db_user"; + String dbPassword = "db_password"; + String dbName = "db_does_not_exist"; + String dbConnectionUri = "postgresql://" + dbUser + ":" + dbPassword + "@localhost:5432/" + dbName; + + Utils.setValueInConfig("postgresql_connection_uri", dbConnectionUri); + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.INIT_FAILURE)); + + Start start = (Start) StorageLayer.getStorage(process.getProcess()); + PostgreSQLConfig userConfig = io.supertokens.storage.postgresql.config.Config.getConfig(start); + String dbPasswordFromConfig = userConfig.getPassword(); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + + assertFalse(fileContainsString(stdOutput, dbPasswordFromConfig)); + assertFalse(fileContainsString(errorOutput, dbPasswordFromConfig)); + } + + } finally { + System.setOut(new PrintStream(new FileOutputStream(FileDescriptor.out))); + System.setErr(new PrintStream(new FileOutputStream(FileDescriptor.err))); + } + } + + @Test + public void testDBPasswordIsNotLoggedWhenTenantIsCreated() throws Exception { + String[] args = { "../" }; + + Utils.setValueInConfig("error_log_path", "null"); + Utils.setValueInConfig("info_log_path", "null"); + + ByteArrayOutputStream stdOutput = new ByteArrayOutputStream(); + ByteArrayOutputStream errorOutput = new ByteArrayOutputStream(); + + System.setOut(new PrintStream(stdOutput)); + System.setErr(new PrintStream(errorOutput)); + + try { + // Case 1: DB Password shouldn't be logged when tenant is created with valid credentials + { + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + Start start = (Start) StorageLayer.getStorage(process.getProcess()); + PostgreSQLConfig userConfig = io.supertokens.storage.postgresql.config.Config.getConfig(start); + String dbPasswordFromConfig = userConfig.getPassword(); + + Main main = process.getProcess(); + + FeatureFlagTestContent.getInstance(main) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[] { + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY }); + + JsonObject config = new JsonObject(); + TenantIdentifier tenantIdentifier = new TenantIdentifier(null, "a1", null); + StorageLayer.getBaseStorage(process.getProcess()).modifyConfigToAddANewUserPoolForTesting(config, 1); + Multitenancy.addNewOrUpdateAppOrTenant( + main, + new TenantIdentifier(null, null, null), + new TenantConfig( + tenantIdentifier, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + null, null, config + )); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + + assertFalse(fileContainsString(stdOutput, dbPasswordFromConfig)); + assertFalse(fileContainsString(errorOutput, dbPasswordFromConfig)); + } + + // Case 2: DB Password shouldn't be logged when tenant is created with invalid credentials + { + String dbUser = "db_user"; + String dbPassword = "db_password"; + String dbName = "db_does_not_exist"; + String dbConnectionUri = "postgresql://" + dbUser + ":" + dbPassword + "@localhost:5432/" + dbName; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + Start start = (Start) StorageLayer.getStorage(process.getProcess()); + PostgreSQLConfig userConfig = io.supertokens.storage.postgresql.config.Config.getConfig(start); + String dbPasswordFromConfig = userConfig.getPassword(); + + Main main = process.getProcess(); + + FeatureFlagTestContent.getInstance(main) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[] { + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY }); + + TenantIdentifier tenantIdentifier = new TenantIdentifier(null, "a1", null); + JsonObject config = new JsonObject(); + config.addProperty("postgresql_connection_uri", dbConnectionUri); + StorageLayer.getBaseStorage(process.getProcess()).modifyConfigToAddANewUserPoolForTesting(config, 1); + try { + Multitenancy.addNewOrUpdateAppOrTenant( + main, + new TenantIdentifier(null, null, null), + new TenantConfig( + tenantIdentifier, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + null, null, + new JsonObject())); + + } catch (Exception e) { + + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + + assertFalse(fileContainsString(stdOutput, dbPasswordFromConfig)); + assertFalse(fileContainsString(errorOutput, dbPasswordFromConfig)); + } + + } finally { + System.setOut(new PrintStream(new FileOutputStream(FileDescriptor.out))); + System.setErr(new PrintStream(new FileOutputStream(FileDescriptor.err))); + } + } + private static int countAppenders(ch.qos.logback.classic.Logger logger) { int count = 0; Iterator> appenderIter = logger.iteratorForAppenders(); diff --git a/src/test/java/io/supertokens/storage/postgresql/test/SuperTokensSaaSSecretTest.java b/src/test/java/io/supertokens/storage/postgresql/test/SuperTokensSaaSSecretTest.java index 7c103e77..51673061 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/SuperTokensSaaSSecretTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/SuperTokensSaaSSecretTest.java @@ -45,12 +45,12 @@ public class SuperTokensSaaSSecretTest { private final String[] PROTECTED_DB_CONFIG = new String[]{"postgresql_connection_pool_size", "postgresql_connection_uri", "postgresql_host", "postgresql_port", "postgresql_user", - "postgresql_password", - "postgresql_database_name", "postgresql_table_schema"}; + "postgresql_password", "postgresql_database_name", "postgresql_table_schema", + "postgresql_minimum_idle_connections", "postgresql_idle_connection_timeout"}; private final Object[] PROTECTED_DB_CONFIG_VALUES = new Object[]{11, "postgresql://root:root@localhost:5432/supertokens?currentSchema=myschema", "localhost", 5432, "root", - "root", "supertokens", "myschema"}; + "root", "supertokens", "myschema", 5, 120000}; @Rule public TestRule watchman = Utils.getOnFailure(); From 85b4e525db8d17f1f1960abff2696664d82407f2 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Thu, 7 Mar 2024 17:54:15 +0530 Subject: [PATCH 099/106] merge latest (#204) * fix: remove db password from logs (#181) * fix: remove db password from logs * fix: Update version * fix: mask db password * fix: Add tests * fix: Add more tests * fix: PR changes * fix: PR changes * fix: Connection pool issue (#182) * fix: test connection pool * fix: changelog * fix: test for downtime during connection pool change * fix: assert that there should be down time * fix: cleanup * adding dev-v5.0.7 tag to this commit to ensure building * fix: cicd tests (#185) * fix: logging test (#187) * adding dev-v5.0.7 tag to this commit to ensure building * fix: flaky test (#188) * adding dev-v5.0.7 tag to this commit to ensure building * fix: adds idle timeout and minimum idle configs (#184) * fix: adds idle timeout and minimum idle configs * fix: protected props * fix: changelog * fix: test protected config * adding dev-v5.0.7 tag to this commit to ensure building * fix: cicd (#189) * fix: cicd * fix: test * adding dev-v5.0.7 tag to this commit to ensure building * fixes tests * adding dev-v5.0.7 tag to this commit to ensure building * fix: vulnerability fix (#192) * fix: vulnerability fix * fix: vulnerability fix * adding dev-v5.0.8 tag to this commit to ensure building * fix: dependencies (#195) * adding dev-v5.0.8 tag to this commit to ensure building * fix: version update (#198) * adding dev-v5.0.8 tag to this commit to ensure building * fix: fixes storage handling for non-auth recipes (#203) * fix: tests * fix: user role table constraint * fix: pr comments * fix: according to updated interface * fix: user roles * fix: version and changelog * fix: plugin interface version * adding dev-v6.0.0 tag to this commit to ensure building --------- Co-authored-by: Ankit Tiwari Co-authored-by: rishabhpoddar --- CHANGELOG.md | 11 +++++++ build.gradle | 2 +- ...-5.0.8.jar => postgresql-plugin-6.0.0.jar} | Bin 213545 -> 213610 bytes pluginInterfaceSupported.json | 2 +- .../supertokens/storage/postgresql/Start.java | 15 +++++++--- .../postgresql/queries/UserRolesQueries.java | 18 +++++++++--- .../postgresql/test/AccountLinkingTests.java | 4 +-- .../postgresql/test/DbConnectionPoolTest.java | 9 +++--- .../storage/postgresql/test/LoggingTest.java | 2 +- .../test/multitenancy/StorageLayerTest.java | 6 ++-- .../TestUserPoolIdChangeBehaviour.java | 27 ++++++++++-------- 11 files changed, 64 insertions(+), 32 deletions(-) rename jar/{postgresql-plugin-5.0.8.jar => postgresql-plugin-6.0.0.jar} (79%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 627fa56f..722bad31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,17 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - This enables smooth switching between `useDynamicAccessTokenSigningKey` settings by allowing refresh calls to change the signing key type of a session +## [6.0.0] - 2024-03-05 + +- Implements `deleteAllUserRoleAssociationsForRole` +- Drops `(app_id, role)` foreign key constraint on `user_roles` table + +### Migration + +```sql +ALTER TABLE user_roles DROP CONSTRAINT IF EXISTS user_roles_role_fkey; +``` + ## [5.0.8] - 2024-02-19 - Fixes vulnerabilities in dependencies diff --git a/build.gradle b/build.gradle index 713fefbe..baafed34 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'java-library' } -version = "5.0.8" +version = "6.0.0" repositories { mavenCentral() diff --git a/jar/postgresql-plugin-5.0.8.jar b/jar/postgresql-plugin-6.0.0.jar similarity index 79% rename from jar/postgresql-plugin-5.0.8.jar rename to jar/postgresql-plugin-6.0.0.jar index d1a855bd6fe0aa5d60dd5df094cced1d43cfef0b..becbc9fa1719dd4cde059d76545a7257bfbfd220 100644 GIT binary patch delta 36179 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 delta 36165 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 diff --git a/pluginInterfaceSupported.json b/pluginInterfaceSupported.json index a5fdc62c..e9d4c148 100644 --- a/pluginInterfaceSupported.json +++ b/pluginInterfaceSupported.json @@ -1,6 +1,6 @@ { "_comment": "contains a list of plugin interfaces branch names that this core supports", "versions": [ - "4.0" + "5.0" ] } \ No newline at end of file diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index c5a72782..cf611631 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -1910,7 +1910,7 @@ public int deleteUserMetadata(AppIdentifier appIdentifier, String userId) throws @Override public void addRoleToUser(TenantIdentifier tenantIdentifier, String userId, String role) - throws StorageQueryException, UnknownRoleException, DuplicateUserRoleMappingException, + throws StorageQueryException, DuplicateUserRoleMappingException, TenantOrAppNotFoundException { try { UserRolesQueries.addRoleToUser(this, tenantIdentifier, userId, role); @@ -1918,9 +1918,6 @@ public void addRoleToUser(TenantIdentifier tenantIdentifier, String userId, Stri if (e instanceof PSQLException) { PostgreSQLConfig config = Config.getConfig(this); ServerErrorMessage serverErrorMessage = ((PSQLException) e).getServerErrorMessage(); - if (isForeignKeyConstraintError(serverErrorMessage, config.getUserRolesTable(), "role")) { - throw new UnknownRoleException(); - } if (isPrimaryKeyError(serverErrorMessage, config.getUserRolesTable())) { throw new DuplicateUserRoleMappingException(); } @@ -1992,6 +1989,16 @@ public boolean deleteRole(AppIdentifier appIdentifier, String role) throws Stora } } + @Override + public boolean deleteAllUserRoleAssociationsForRole(AppIdentifier appIdentifier, String role) + throws StorageQueryException { + try { + return UserRolesQueries.deleteAllUserRoleAssociationsForRole(this, appIdentifier, role); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + @Override public String[] getRoles(AppIdentifier appIdentifier) throws StorageQueryException { try { diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/UserRolesQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/UserRolesQueries.java index 549cac86..10fcb1a7 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/UserRolesQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/UserRolesQueries.java @@ -17,8 +17,10 @@ package io.supertokens.storage.postgresql.queries; import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.sqlStorage.TransactionConnection; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.config.Config; import io.supertokens.storage.postgresql.utils.Utils; @@ -91,9 +93,6 @@ public static String getQueryToCreateUserRolesTable(Start start) { + "role VARCHAR(255) NOT NULL," + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, null, "pkey") + " PRIMARY KEY(app_id, tenant_id, user_id, role)," - + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, "role", "fkey") - + " FOREIGN KEY(app_id, role)" - + " REFERENCES " + getConfig(start).getRolesTable() + "(app_id, role) ON DELETE CASCADE," + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, "tenant_id", "fkey") + " FOREIGN KEY (app_id, tenant_id)" + " REFERENCES " + Config.getConfig(start).getTenantsTable() + "(app_id, tenant_id) ON DELETE CASCADE" @@ -142,7 +141,8 @@ public static void addPermissionToRoleOrDoNothingIfExists_Transaction(Start star }); } - public static boolean deleteRole(Start start, AppIdentifier appIdentifier, String role) + public static boolean deleteRole(Start start, AppIdentifier appIdentifier, + String role) throws SQLException, StorageQueryException { String QUERY = "DELETE FROM " + getConfig(start).getRolesTable() + " WHERE app_id = ? AND role = ? ;"; @@ -353,4 +353,14 @@ public static int deleteAllRolesForUser_Transaction(Connection con, Start start, pst.setString(2, userId); }); } + + public static boolean deleteAllUserRoleAssociationsForRole(Start start, AppIdentifier appIdentifier, String role) + throws SQLException, StorageQueryException { + String QUERY = "DELETE FROM " + getConfig(start).getUserRolesTable() + + " WHERE app_id = ? AND role = ? ;"; + return update(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, role); + }) >= 1; + } } diff --git a/src/test/java/io/supertokens/storage/postgresql/test/AccountLinkingTests.java b/src/test/java/io/supertokens/storage/postgresql/test/AccountLinkingTests.java index 73b4728a..5ab09431 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/AccountLinkingTests.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/AccountLinkingTests.java @@ -98,7 +98,7 @@ public void canLinkFailsIfTryingToLinkUsersAcrossDifferentStorageLayers() throws AuthRecipe.createPrimaryUser(process.main, user1.getSupertokensUserId()); AuthRecipeUserInfo user2 = EmailPassword.signUp( - tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, process.main)), + tenantIdentifier, (StorageLayer.getStorage(tenantIdentifier, process.main)), process.getProcess(), "test2@example.com", "abcd1234"); try { @@ -137,7 +137,7 @@ public void canLinkFailsIfTryingToLinkUsersAcrossDifferentStorageLayers() throws ); AuthRecipeUserInfo user3 = EmailPassword.signUp( - tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, process.main)), + tenantIdentifier, (StorageLayer.getStorage(tenantIdentifier, process.main)), process.getProcess(), "test2@example.com", "abcd1234"); Map params = new HashMap<>(); diff --git a/src/test/java/io/supertokens/storage/postgresql/test/DbConnectionPoolTest.java b/src/test/java/io/supertokens/storage/postgresql/test/DbConnectionPoolTest.java index d214d1bc..470e6ce0 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/DbConnectionPoolTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/DbConnectionPoolTest.java @@ -24,6 +24,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; +import io.supertokens.pluginInterface.Storage; import io.supertokens.pluginInterface.multitenancy.*; import org.junit.AfterClass; import org.junit.Before; @@ -152,8 +153,8 @@ public void testDownTimeWhenChangingConnectionPoolSize() throws Exception { es.execute(() -> { try { TenantIdentifier t1 = new TenantIdentifier(null, null, "t1"); - TenantIdentifierWithStorage t1WithStorage = t1.withStorage(StorageLayer.getStorage(t1, process.getProcess())); - ThirdParty.signInUp(t1WithStorage, process.getProcess(), "google", "googleid"+ finalI, "user" + + Storage t1Storage = (StorageLayer.getStorage(t1, process.getProcess())); + ThirdParty.signInUp(t1, t1Storage, process.getProcess(), "google", "googleid"+ finalI, "user" + finalI + "@example.com"); if (firstErrorTime.get() != -1 && successAfterErrorTime.get() == -1) { @@ -353,8 +354,8 @@ public void testIdleConnectionTimeout() throws Exception { es.execute(() -> { try { TenantIdentifier t1 = new TenantIdentifier(null, null, "t1"); - TenantIdentifierWithStorage t1WithStorage = t1.withStorage(StorageLayer.getStorage(t1, process.getProcess())); - ThirdParty.signInUp(t1WithStorage, process.getProcess(), "google", "googleid"+ finalI, "user" + + Storage t1Storage = (StorageLayer.getStorage(t1, process.getProcess())); + ThirdParty.signInUp(t1, t1Storage, process.getProcess(), "google", "googleid"+ finalI, "user" + finalI + "@example.com"); } catch (StorageQueryException e) { diff --git a/src/test/java/io/supertokens/storage/postgresql/test/LoggingTest.java b/src/test/java/io/supertokens/storage/postgresql/test/LoggingTest.java index a96a91c8..591a4ac0 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/LoggingTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/LoggingTest.java @@ -520,7 +520,7 @@ public void testDBPasswordIsNotLoggedWhenTenantIsCreated() throws Exception { new ThirdPartyConfig(true, null), new PasswordlessConfig(true), null, null, config - )); + )); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); diff --git a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java index 1b967b85..77204865 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java @@ -808,7 +808,7 @@ public void testTenantCreationAndThenDbDownDbThrowsErrorInRecipesAndDoesntAffect MultitenancyHelper.getInstance(process.getProcess()).refreshTenantsInCoreBasedOnChangesInCoreConfigOrIfTenantListChanged(true); try { - EmailPassword.signIn(tid.withStorage(StorageLayer.getStorage(tid, process.getProcess())), + EmailPassword.signIn(tid, (StorageLayer.getStorage(tid, process.getProcess())), process.getProcess(), "", ""); fail(); } catch (StorageQueryException e) { @@ -821,7 +821,7 @@ public void testTenantCreationAndThenDbDownDbThrowsErrorInRecipesAndDoesntAffect // we do this again just to check that if this function is called again, it fails again and there is no // side effect of calling the above function try { - EmailPassword.signIn(tid.withStorage(StorageLayer.getStorage(tid, process.getProcess())), + EmailPassword.signIn(tid, (StorageLayer.getStorage(tid, process.getProcess())), process.getProcess(), "", ""); fail(); } catch (StorageQueryException e) { @@ -850,7 +850,7 @@ public void testTenantCreationAndThenDbDownDbThrowsErrorInRecipesAndDoesntAffect TenantIdentifier tid = new TenantIdentifier("abc", null, null); try { - EmailPassword.signIn(tid.withStorage(StorageLayer.getStorage(tid, process.getProcess())), + EmailPassword.signIn(tid, (StorageLayer.getStorage(tid, process.getProcess())), process.getProcess(), "", ""); fail(); } catch (StorageQueryException e) { diff --git a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/TestUserPoolIdChangeBehaviour.java b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/TestUserPoolIdChangeBehaviour.java index 51284930..758e749a 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/TestUserPoolIdChangeBehaviour.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/TestUserPoolIdChangeBehaviour.java @@ -25,6 +25,7 @@ import io.supertokens.multitenancy.Multitenancy; import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.multitenancy.exception.CannotModifyBaseConfigException; +import io.supertokens.pluginInterface.Storage; import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.exceptions.InvalidConfigException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; @@ -87,13 +88,13 @@ public void testUsersWorkAfterUserPoolIdChanges() throws Exception { coreConfig ), false); - TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage( + Storage storage = ( StorageLayer.getStorage(tenantIdentifier, process.getProcess())); - String userPoolId = tenantIdentifierWithStorage.getStorage().getUserPoolId(); + String userPoolId = storage.getUserPoolId(); AuthRecipeUserInfo userInfo = EmailPassword.signUp( - tenantIdentifierWithStorage, process.getProcess(), "user@example.com", "password"); + tenantIdentifier, storage, process.getProcess(), "user@example.com", "password"); coreConfig.addProperty("postgresql_host", "127.0.0.1"); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( @@ -105,12 +106,13 @@ public void testUsersWorkAfterUserPoolIdChanges() throws Exception { coreConfig ), false); - tenantIdentifierWithStorage = tenantIdentifier.withStorage( + storage = ( StorageLayer.getStorage(tenantIdentifier, process.getProcess())); - String userPoolId2 = tenantIdentifierWithStorage.getStorage().getUserPoolId(); + String userPoolId2 = storage.getUserPoolId(); assertNotEquals(userPoolId, userPoolId2); - AuthRecipeUserInfo user2 = EmailPassword.signIn(tenantIdentifierWithStorage, process.getProcess(), + AuthRecipeUserInfo user2 = EmailPassword.signIn( + tenantIdentifier, storage, process.getProcess(), "user@example.com", "password"); assertEquals(userInfo, user2); @@ -133,13 +135,13 @@ public void testUsersWorkAfterUserPoolIdChangesAndServerRestart() throws Excepti coreConfig ), false); - TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage( + Storage storage = ( StorageLayer.getStorage(tenantIdentifier, process.getProcess())); - String userPoolId = tenantIdentifierWithStorage.getStorage().getUserPoolId(); + String userPoolId = storage.getUserPoolId(); AuthRecipeUserInfo userInfo = EmailPassword.signUp( - tenantIdentifierWithStorage, process.getProcess(), "user@example.com", "password"); + tenantIdentifier, storage, process.getProcess(), "user@example.com", "password"); coreConfig.addProperty("postgresql_host", "127.0.0.1"); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( @@ -157,12 +159,13 @@ public void testUsersWorkAfterUserPoolIdChangesAndServerRestart() throws Excepti this.process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - tenantIdentifierWithStorage = tenantIdentifier.withStorage( + storage = ( StorageLayer.getStorage(tenantIdentifier, process.getProcess())); - String userPoolId2 = tenantIdentifierWithStorage.getStorage().getUserPoolId(); + String userPoolId2 = storage.getUserPoolId(); assertNotEquals(userPoolId, userPoolId2); - AuthRecipeUserInfo user2 = EmailPassword.signIn(tenantIdentifierWithStorage, process.getProcess(), + AuthRecipeUserInfo user2 = EmailPassword.signIn( + tenantIdentifier, storage, process.getProcess(), "user@example.com", "password"); From f24e2eb9c7ccf8aedfa1fc152afc381c33ff8dc6 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Mon, 11 Mar 2024 11:56:40 +0530 Subject: [PATCH 100/106] fix: One million users test (#196) * test: one million users first version * fix: user data * fix: update test * fix: update cicd * fix: wip * fix: measurements * fix: test * fix: adding memory tests * fix: memory limit --- .circleci/config.yml | 103 +- .circleci/doOneMillionUsersTests.sh | 135 +++ .circleci/doTests.sh | 19 +- .circleci/markPassed.sh | 29 + .github/PULL_REQUEST_TEMPLATE.md | 2 +- .../postgresql/test/OneMillionUsersTest.java | 912 ++++++++++++++++++ 6 files changed, 1181 insertions(+), 19 deletions(-) create mode 100755 .circleci/doOneMillionUsersTests.sh create mode 100755 .circleci/markPassed.sh create mode 100644 src/test/java/io/supertokens/storage/postgresql/test/OneMillionUsersTest.java diff --git a/.circleci/config.yml b/.circleci/config.yml index 02c3a060..8fd9177a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -77,6 +77,88 @@ jobs: name: running tests command: (cd .circleci/ && ./doTests.sh) - slack/status + test-onemillionusers: + docker: + - image: rishabhpoddar/supertokens_postgresql_plugin_test + resource_class: large + steps: + - add_ssh_keys: + fingerprints: + - "14:68:18:82:73:00:e4:fc:9e:f3:6f:ce:1d:5c:6d:c4" + - checkout + - run: + name: update postgresql max_connections + command: | + sed -i 's/^#*\s*max_connections\s*=.*/max_connections = 10000/' /etc/postgresql/9.5/main/postgresql.conf + - run: + name: starting postgresql + command: | + (cd / && ./runPostgreSQL.sh) + - run: + name: create databases + command: | + psql -c "create database st0;" + psql -c "create database st1;" + psql -c "create database st2;" + psql -c "create database st3;" + psql -c "create database st4;" + psql -c "create database st5;" + psql -c "create database st6;" + psql -c "create database st7;" + psql -c "create database st8;" + psql -c "create database st9;" + psql -c "create database st10;" + psql -c "create database st11;" + psql -c "create database st12;" + psql -c "create database st13;" + psql -c "create database st14;" + psql -c "create database st15;" + psql -c "create database st16;" + psql -c "create database st17;" + psql -c "create database st18;" + psql -c "create database st19;" + psql -c "create database st20;" + psql -c "create database st21;" + psql -c "create database st22;" + psql -c "create database st23;" + psql -c "create database st24;" + psql -c "create database st25;" + psql -c "create database st26;" + psql -c "create database st27;" + psql -c "create database st28;" + psql -c "create database st29;" + psql -c "create database st30;" + psql -c "create database st31;" + psql -c "create database st32;" + psql -c "create database st33;" + psql -c "create database st34;" + psql -c "create database st35;" + psql -c "create database st36;" + psql -c "create database st37;" + psql -c "create database st38;" + psql -c "create database st39;" + psql -c "create database st40;" + psql -c "create database st41;" + psql -c "create database st42;" + psql -c "create database st43;" + psql -c "create database st44;" + psql -c "create database st45;" + psql -c "create database st46;" + psql -c "create database st47;" + psql -c "create database st48;" + psql -c "create database st49;" + psql -c "create database st50;" + - run: + name: running tests + command: (cd .circleci/ && ./doOneMillionUsersTests.sh) + - slack/status + mark-passed: + docker: + - image: rishabhpoddar/supertokens_postgresql_plugin_test + steps: + - checkout + - run: (cd .circleci && ./markPassed.sh) + - slack/status workflows: version: 2 @@ -89,4 +171,23 @@ workflows: tags: only: /dev-v[0-9]+(\.[0-9]+)*/ branches: - ignore: /.*/ \ No newline at end of file + ignore: /.*/ + - test-onemillionusers: + context: + - slack-notification + filters: + tags: + only: /dev-v[0-9]+(\.[0-9]+)*/ + branches: + ignore: /.*/ + - mark-passed: + context: + - slack-notification + filters: + tags: + only: /dev-v[0-9]+(\.[0-9]+)*/ + branches: + ignore: /.*/ + requires: + - test + - test-onemillionusers diff --git a/.circleci/doOneMillionUsersTests.sh b/.circleci/doOneMillionUsersTests.sh new file mode 100755 index 00000000..ec82508d --- /dev/null +++ b/.circleci/doOneMillionUsersTests.sh @@ -0,0 +1,135 @@ +function cleanup { + if test -f "pluginInterfaceExactVersionsOutput"; then + rm pluginInterfaceExactVersionsOutput + fi +} + +trap cleanup EXIT +cleanup + +pluginInterfaceJson=`cat ../pluginInterfaceSupported.json` +pluginInterfaceLength=`echo $pluginInterfaceJson | jq ".versions | length"` +pluginInterfaceArray=`echo $pluginInterfaceJson | jq ".versions"` +echo "got plugin interface relations" + +./getPluginInterfaceExactVersions.sh $pluginInterfaceLength "$pluginInterfaceArray" + +if [[ $? -ne 0 ]] +then + echo "all plugin interfaces found... failed. exiting!" + exit 1 +else + echo "all plugin interfaces found..." +fi + +# get plugin version +pluginVersion=`cat ../build.gradle | grep -e "version =" -e "version="` +while IFS='"' read -ra ADDR; do + counter=0 + for i in "${ADDR[@]}"; do + if [ $counter == 1 ] + then + pluginVersion=$i + fi + counter=$(($counter+1)) + done +done <<< "$pluginVersion" + +responseStatus=`curl -s -o /dev/null -w "%{http_code}" -X PUT \ + https://api.supertokens.io/0/plugin \ + -H 'Content-Type: application/json' \ + -H 'api-version: 0' \ + -d "{ + \"password\": \"$SUPERTOKENS_API_KEY\", + \"planType\":\"FREE\", + \"version\":\"$pluginVersion\", + \"pluginInterfaces\": $pluginInterfaceArray, + \"name\": \"postgresql\" +}"` +if [ $responseStatus -ne "200" ] +then + echo "failed plugin PUT API status code: $responseStatus. Exiting!" + exit 1 +fi + +someTestsRan=false +while read -u 10 line +do + if [[ $line = "" ]]; then + continue + fi + i=0 + currTag=`echo $line | jq .tag` + currTag=`echo $currTag | tr -d '"'` + + currVersion=`echo $line | jq .version` + currVersion=`echo $currVersion | tr -d '"'` + piX=$(cut -d'.' -f1 <<<"$currVersion") + piY=$(cut -d'.' -f2 <<<"$currVersion") + piVersion="$piX.$piY" + + someTestsRan=true + + response=`curl -s -X GET \ + "https://api.supertokens.io/0/plugin-interface/dependency/core/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$piVersion" \ + -H 'api-version: 0'` + if [[ `echo $response | jq .core` == "null" ]] + then + echo "fetching latest X.Y version for core given plugin-interface X.Y version: $piVersion gave response: $response" + exit 1 + fi + coreVersionX2=$(echo $response | jq .core | tr -d '"') + + response=`curl -s -X GET \ + "https://api.supertokens.io/0/core/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$coreVersionX2" \ + -H 'api-version: 0'` + if [[ `echo $response | jq .tag` == "null" ]] + then + echo "fetching latest X.Y.Z version for core X.Y version: $coreVersionX2 gave response: $response" + exit 1 + fi + coreVersionTag=$(echo $response | jq .tag | tr -d '"') + + cd ../../ + git clone git@github.com:supertokens/supertokens-root.git + cd supertokens-root + + update-alternatives --install "/usr/bin/java" "java" "/usr/java/jdk-15.0.1/bin/java" 2 + update-alternatives --install "/usr/bin/javac" "javac" "/usr/java/jdk-15.0.1/bin/javac" 2 + + pluginX=$(cut -d'.' -f1 <<<"$pluginVersion") + pluginY=$(cut -d'.' -f2 <<<"$pluginVersion") + echo -e "core,$coreVersionX2\nplugin-interface,$piVersion\npostgresql-plugin,$pluginX.$pluginY" > modules.txt + ./loadModules + cd supertokens-core + git checkout $coreVersionTag + cd ../supertokens-plugin-interface + git checkout $currTag + cd ../supertokens-postgresql-plugin + git checkout dev-v$pluginVersion + cd ../ + echo $SUPERTOKENS_API_KEY > apiPassword + export ONE_MILLION_USERS_TEST=1 + ./utils/setupTestEnv --cicd + ./gradlew :supertokens-postgresql-plugin:test --tests io.supertokens.storage.postgresql.test.OneMillionUsersTest + + if [[ $? -ne 0 ]] + then + cat logs/* + cd ../project/ + echo "test failed... exiting!" + exit 1 + fi + cd ../ + rm -rf supertokens-root + cd project/.circleci +done 10 { + String userId = io.supertokens.utils.Utils.getUUID(); + long timeJoined = System.currentTimeMillis(); + try { + storage.createUser(TenantIdentifier.BASE_TENANT, userId, "pltest" + finalI + "@example.com", null, timeJoined); + } catch (Exception e) { + throw new RuntimeException(e); + } + + if (finalI % 10000 == 9999) { + System.out.println("Created " + ((finalI +1)) + " users"); + } + }); + } + + es.shutdown(); + es.awaitTermination(10, TimeUnit.MINUTES); + } + + private void createPasswordlessUsersWithPhone(Main main) throws Exception { + System.out.println("Creating passwordless (phone) users..."); + + ExecutorService es = Executors.newFixedThreadPool(NUM_THREADS); + PasswordlessSQLStorage storage = (PasswordlessSQLStorage) StorageLayer.getBaseStorage(main); + + for (int i = 0; i < TOTAL_USERS / 4; i++) { + int finalI = i; + es.execute(() -> { + String userId = io.supertokens.utils.Utils.getUUID(); + long timeJoined = System.currentTimeMillis(); + try { + storage.createUser(TenantIdentifier.BASE_TENANT, userId, null, "+91987654" + finalI, timeJoined); + } catch (Exception e) { + throw new RuntimeException(e); + } + + if (finalI % 10000 == 9999) { + System.out.println("Created " + ((finalI +1)) + " users"); + } + }); + } + + es.shutdown(); + es.awaitTermination(10, TimeUnit.MINUTES); + } + + private void createThirdpartyUsers(Main main) throws Exception { + System.out.println("Creating thirdparty users..."); + + ExecutorService es = Executors.newFixedThreadPool(NUM_THREADS); + ThirdPartySQLStorage storage = (ThirdPartySQLStorage) StorageLayer.getBaseStorage(main); + + for (int i = 0; i < TOTAL_USERS / 4; i++) { + int finalI = i; + es.execute(() -> { + String userId = io.supertokens.utils.Utils.getUUID(); + long timeJoined = System.currentTimeMillis(); + + try { + storage.signUp(TenantIdentifier.BASE_TENANT, userId, "tptest" + finalI + "@example.com", new LoginMethod.ThirdParty("google", "googleid" + finalI), timeJoined ); + } catch (Exception e) { + throw new RuntimeException(e); + } + + if (finalI % 10000 == 9999) { + System.out.println("Created " + (finalI +1) + " users"); + } + }); + } + + es.shutdown(); + es.awaitTermination(10, TimeUnit.MINUTES); + } + + private void createOneMillionUsers(Main main) throws Exception { + Thread.sleep(5000); + + createEmailPasswordUsers(main); + createPasswordlessUsersWithEmail(main); + createPasswordlessUsersWithPhone(main); + createThirdpartyUsers(main); + } + + private void createUserIdMappings(Main main) throws Exception { + System.out.println("Creating user id mappings..."); + + ExecutorService es = Executors.newFixedThreadPool(NUM_THREADS); + + UserPaginationContainer usersResult = AuthRecipe.getUsers(main, 10000, "ASC", null, + null, null); + + AtomicLong usersUpdated = new AtomicLong(0); + + while (true) { + for (AuthRecipeUserInfo user : usersResult.users) { + es.execute(() -> { + Random random = new Random(); + + // UserId mapping + for (LoginMethod lm : user.loginMethods) { + String userId = user.getSupertokensUserId(); + + if (random.nextBoolean()) { + userId = "ext" + UUID.randomUUID().toString(); + try { + UserIdMapping.createUserIdMapping(main, lm.getSupertokensUserId(), userId, null, false); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + long count = usersUpdated.incrementAndGet(); + if (count % 10000 == 9999) { + System.out.println("Updated " + (count) + " users"); + } + } + }); + } + if (usersResult.nextPaginationToken == null) { + break; + } + usersResult = AuthRecipe.getUsers(main, 10000, "ASC", usersResult.nextPaginationToken, + null, null); + } + + es.shutdown(); + es.awaitTermination(10, TimeUnit.MINUTES); + } + + private void createUserData(Main main) throws Exception { + System.out.println("Creating user data..."); + + ExecutorService es = Executors.newFixedThreadPool(NUM_THREADS / 2); + + UserPaginationContainer usersResult = AuthRecipe.getUsers(main, 500, "ASC", null, + null, null); + + while (true) { + UserIdMapping.populateExternalUserIdForUsers( + (StorageLayer.getBaseStorage(main)), + usersResult.users); + + for (AuthRecipeUserInfo user : usersResult.users) { + es.execute(() -> { + Random random = new Random(); + + // User Metadata + JsonObject metadata = new JsonObject(); + metadata.addProperty("random", random.nextDouble()); + + try { + UserMetadata.updateUserMetadata(main, user.getSupertokensOrExternalUserId(), metadata); + + // User Roles + if (random.nextBoolean()) { + UserRoles.addRoleToUser(main, user.getSupertokensOrExternalUserId(), "admin"); + } else { + UserRoles.addRoleToUser(main, user.getSupertokensOrExternalUserId(), "user"); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + + if (usersResult.nextPaginationToken == null) { + break; + } + usersResult = AuthRecipe.getUsers(main, 500, "ASC", usersResult.nextPaginationToken, + null, null); + } + + es.shutdown(); + es.awaitTermination(1, TimeUnit.MINUTES); + } + + private void doAccountLinking(Main main) throws Exception { + Set userIds = new HashSet<>(); + + long st = System.currentTimeMillis(); + UserPaginationContainer usersResult = AuthRecipe.getUsers(main, 1000, "ASC", null, + null, null); + + while (true) { + for (AuthRecipeUserInfo user : usersResult.users) { + userIds.add(user.getSupertokensUserId()); + } + if (usersResult.nextPaginationToken == null) { + break; + } + usersResult = AuthRecipe.getUsers(main, 1000, "ASC", usersResult.nextPaginationToken, + null, null); + } + + long en = System.currentTimeMillis(); + + System.out.println("Time taken to get " + TOTAL_USERS + " users (before account linking): " + ((en - st) / 1000) + " sec"); + + assertEquals(TOTAL_USERS, userIds.size()); + + AtomicLong accountsLinked = new AtomicLong(0); + + ExecutorService es = Executors.newFixedThreadPool(NUM_THREADS); + + while (userIds.size() > 0) { + int numberOfItemsToPick = Math.min(new Random().nextInt(4) + 1, userIds.size()); + String[] userIdsArray = new String[numberOfItemsToPick]; + + Iterator iterator = userIds.iterator(); + for (int i = 0; i < numberOfItemsToPick; i++) { + userIdsArray[i] = iterator.next(); + iterator.remove(); + } + + AuthRecipeSQLStorage storage = (AuthRecipeSQLStorage) StorageLayer.getBaseStorage(main); + + es.execute(() -> { + try { + storage.startTransaction(con -> { + storage.makePrimaryUser_Transaction(new AppIdentifier(null, null), con, userIdsArray[0]); + storage.commitTransaction(con); + return null; + }); + } catch (Exception e) { + throw new RuntimeException(e); + } + + try { + for (int i = 1; i < userIdsArray.length; i++) { + int finalI = i; + storage.startTransaction(con -> { + storage.linkAccounts_Transaction(new AppIdentifier(null, null), con, userIdsArray[finalI], + userIdsArray[0]); + storage.commitTransaction(con); + return null; + }); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + + long total = accountsLinked.addAndGet(userIdsArray.length); + if (total % 10000 > 9996) { + System.out.println("Linked " + (accountsLinked) + " users"); + } + }); + } + + es.shutdown(); + es.awaitTermination(10, TimeUnit.MINUTES); + } + + private static String accessToken; + private static String sessionUserId; + + private void createSessions(Main main) throws Exception { + System.out.println("Creating sessions..."); + + ExecutorService es = Executors.newFixedThreadPool(NUM_THREADS); + + UserPaginationContainer usersResult = AuthRecipe.getUsers(main, 500, "ASC", null, + null, null); + + while (true) { + UserIdMapping.populateExternalUserIdForUsers( + (StorageLayer.getBaseStorage(main)), + usersResult.users); + + for (AuthRecipeUserInfo user : usersResult.users) { + es.execute(() -> { + try { + for (LoginMethod lM : user.loginMethods) { + String userId = lM.getSupertokensOrExternalUserId(); + SessionInformationHolder session = Session.createNewSession(main, + userId, new JsonObject(), new JsonObject()); + + if (new Random().nextFloat() < 0.05) { + accessToken = session.accessToken.token; + sessionUserId = userId; + } + } + + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + if (usersResult.nextPaginationToken == null) { + break; + } + usersResult = AuthRecipe.getUsers(main, 500, "ASC", usersResult.nextPaginationToken, + null, null); + } + + es.shutdown(); + es.awaitTermination(10, TimeUnit.MINUTES); + } + + @Test + public void testCreatingOneMillionUsers() throws Exception { + if (System.getenv("ONE_MILLION_USERS_TEST") == null) { + return; + } + + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + Utils.setValueInConfig("firebase_password_hashing_signer_key", + "gRhC3eDeQOdyEn4bMd9c6kxguWVmcIVq/SKa0JDPFeM6TcEevkaW56sIWfx88OHbJKnCXdWscZx0l2WbCJ1wbg=="); + Utils.setValueInConfig("postgresql_connection_pool_size", "500"); + + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + AtomicBoolean memoryCheckRunning = new AtomicBoolean(true); + AtomicLong maxMemory = new AtomicLong(0); + + { + long st = System.currentTimeMillis(); + createOneMillionUsers(process.getProcess()); + long en = System.currentTimeMillis(); + System.out.println("Time taken to create " + TOTAL_USERS + " users: " + ((en - st) / 1000) + " sec"); + assertEquals(TOTAL_USERS, AuthRecipe.getUsersCount(process.getProcess(), null)); + } + + { + long st = System.currentTimeMillis(); + doAccountLinking(process.getProcess()); + long en = System.currentTimeMillis(); + System.out.println("Time taken to link accounts: " + ((en - st) / 1000) + " sec"); + } + + { + long st = System.currentTimeMillis(); + createUserIdMappings(process.getProcess()); + long en = System.currentTimeMillis(); + System.out.println("Time taken to create user id mappings: " + ((en - st) / 1000) + " sec"); + } + + { + UserRoles.createNewRoleOrModifyItsPermissions(process.getProcess(), "admin", new String[]{"p1"}); + UserRoles.createNewRoleOrModifyItsPermissions(process.getProcess(), "user", new String[]{"p2"}); + long st = System.currentTimeMillis(); + createUserData(process.getProcess()); + long en = System.currentTimeMillis(); + System.out.println("Time taken to create user data: " + ((en - st) / 1000) + " sec"); + } + + { + long st = System.currentTimeMillis(); + createSessions(process.getProcess()); + long en = System.currentTimeMillis(); + System.out.println("Time taken to create sessions: " + ((en - st) / 1000) + " sec"); + } + + sanityCheckAPIs(process.getProcess()); + + Runtime.getRuntime().gc(); + Thread.sleep(10000); + + Thread memoryChecker = new Thread(() -> { + while (memoryCheckRunning.get()) { + Runtime rt = Runtime.getRuntime(); + long total_mem = rt.totalMemory(); + long free_mem = rt.freeMemory(); + long used_mem = total_mem - free_mem; + + if (used_mem > maxMemory.get()) { + maxMemory.set(used_mem); + } + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + }); + memoryChecker.start(); + + measureOperations(process.getProcess()); + + memoryCheckRunning.set(false); + memoryChecker.join(); + + System.out.println("Max memory used: " + (maxMemory.get() / (1024 * 1024)) + " MB"); + assert maxMemory.get() < 320 * 1024 * 1024; // must be less than 320 mb + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + private void sanityCheckAPIs(Main main) throws Exception { + { // Email password sign in + JsonObject responseBody = new JsonObject(); + responseBody.addProperty("email", "eptest10@example.com"); + responseBody.addProperty("password", "testPass123"); + + Thread.sleep(1); // add a small delay to ensure a unique timestamp + long beforeSignIn = System.currentTimeMillis(); + + JsonObject signInResponse = HttpRequestForTesting.sendJsonPOSTRequest(main, "", + "http://localhost:3567/recipe/signin", responseBody, 1000, 1000, null, SemVer.v4_0.get(), + "emailpassword"); + + assertEquals(signInResponse.get("status").getAsString(), "OK"); + assertEquals(signInResponse.entrySet().size(), 3); + + JsonObject jsonUser = signInResponse.get("user").getAsJsonObject(); + JsonArray emails = jsonUser.get("emails").getAsJsonArray(); + boolean found = false; + + for (JsonElement elem : emails) { + if (elem.getAsString().equals("eptest10@example.com")) { + found = true; + break; + } + } + + assertTrue(found); + + int activeUsers = ActiveUsers.countUsersActiveSince(main, beforeSignIn); + assert (activeUsers == 1); + } + + { // passwordless sign in + long startTs = System.currentTimeMillis(); + + String email = "pltest10@example.com"; + Passwordless.CreateCodeResponse createResp = Passwordless.createCode(main, email, null, null, null); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(main, "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + assertEquals("OK", response.get("status").getAsString()); + assertEquals(false, response.get("createdNewUser").getAsBoolean()); + assert (response.has("user")); + + JsonObject jsonUser = response.get("user").getAsJsonObject(); + JsonArray emails = jsonUser.get("emails").getAsJsonArray(); + boolean found = false; + + for (JsonElement elem : emails) { + if (elem.getAsString().equals("pltest10@example.com")) { + found = true; + break; + } + } + + assertTrue(found); + + int activeUsers = ActiveUsers.countUsersActiveSince(main, startTs); + assert (activeUsers == 1); + } + + { // thirdparty sign in + long startTs = System.currentTimeMillis(); + JsonObject emailObject = new JsonObject(); + emailObject.addProperty("id", "tptest10@example.com"); + emailObject.addProperty("isVerified", true); + + JsonObject signUpRequestBody = new JsonObject(); + signUpRequestBody.addProperty("thirdPartyId", "google"); + signUpRequestBody.addProperty("thirdPartyUserId", "googleid10"); + signUpRequestBody.add("email", emailObject); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(main, "", + "http://localhost:3567/recipe/signinup", signUpRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "thirdparty"); + + assertEquals("OK", response.get("status").getAsString()); + assertEquals(false, response.get("createdNewUser").getAsBoolean()); + assert (response.has("user")); + + JsonObject jsonUser = response.get("user").getAsJsonObject(); + JsonArray emails = jsonUser.get("emails").getAsJsonArray(); + boolean found = false; + + for (JsonElement elem : emails) { + if (elem.getAsString().equals("tptest10@example.com")) { + found = true; + break; + } + } + + assertTrue(found); + + int activeUsers = ActiveUsers.countUsersActiveSince(main, startTs); + assert (activeUsers == 1); + } + + { // session for user + JsonObject request = new JsonObject(); + request.addProperty("accessToken", accessToken); + request.addProperty("doAntiCsrfCheck", false); + request.addProperty("enableAntiCsrf", false); + request.addProperty("checkDatabase", false); + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(main, "", + "http://localhost:3567/recipe/session/verify", request, 1000, 1000, null, + SemVer.v5_0.get(), "session"); + assertEquals("OK", response.get("status").getAsString()); + assertEquals(sessionUserId, response.get("session").getAsJsonObject().get("userId").getAsString()); + } + + { // check user roles + JsonObject responseBody = new JsonObject(); + responseBody.addProperty("email", "eptest10@example.com"); + responseBody.addProperty("password", "testPass123"); + + Thread.sleep(1); // add a small delay to ensure a unique timestamp + JsonObject signInResponse = HttpRequestForTesting.sendJsonPOSTRequest(main, "", + "http://localhost:3567/recipe/signin", responseBody, 1000, 1000, null, SemVer.v4_0.get(), + "emailpassword"); + + HashMap QUERY_PARAMS = new HashMap<>(); + QUERY_PARAMS.put("userId", signInResponse.get("user").getAsJsonObject().get("id").getAsString()); + JsonObject response = HttpRequestForTesting.sendGETRequest(main, "", + "http://localhost:3567/recipe/user/roles", QUERY_PARAMS, 1000, 1000, null, + SemVer.v2_14.get(), "userroles"); + + assertEquals(2, response.entrySet().size()); + assertEquals("OK", response.get("status").getAsString()); + + JsonArray userRolesArr = response.getAsJsonArray("roles"); + assertEquals(1, userRolesArr.size()); + assertTrue( + userRolesArr.get(0).getAsString().equals("admin") || userRolesArr.get(0).getAsString().equals("user") + ); + } + + { // check user metadata + HashMap QueryParams = new HashMap(); + QueryParams.put("userId", sessionUserId); + JsonObject resp = HttpRequestForTesting.sendGETRequest(main, "", + "http://localhost:3567/recipe/user/metadata", QueryParams, 1000, 1000, null, + SemVer.v2_13.get(), "usermetadata"); + + assertEquals(2, resp.entrySet().size()); + assertEquals("OK", resp.get("status").getAsString()); + assert (resp.has("metadata")); + JsonObject respMetadata = resp.getAsJsonObject("metadata"); + assertEquals(1, respMetadata.entrySet().size()); + } + } + + private void measureOperations(Main main) throws Exception { + AtomicLong errorCount = new AtomicLong(0); + { // Emailpassword sign up + System.out.println("Measure email password sign-ups"); + long time = measureTime(() -> { + ExecutorService es = Executors.newFixedThreadPool(100); + + for (int i = 0; i < 500; i++) { + int finalI = i; + es.execute(() -> { + try { + EmailPassword.signUp(main, "ep" + finalI + "@example.com", "password" + finalI); + } catch (Exception e) { + errorCount.incrementAndGet(); + throw new RuntimeException(e); + } + }); + } + + es.shutdown(); + try { + es.awaitTermination(5, TimeUnit.MINUTES); + } catch (InterruptedException e) { + errorCount.incrementAndGet(); + throw new RuntimeException(e); + } + return null; + }); + System.out.println("EP sign up " + time); + assert time < 15000; + } + { // Emailpassword sign in + System.out.println("Measure email password sign-ins"); + long time = measureTime(() -> { + ExecutorService es = Executors.newFixedThreadPool(100); + + for (int i = 0; i < 500; i++) { + int finalI = i; + es.execute(() -> { + try { + EmailPassword.signIn(main, "ep" + finalI + "@example.com", "password" + finalI); + } catch (Exception e) { + errorCount.incrementAndGet(); + throw new RuntimeException(e); + } + }); + } + + es.shutdown(); + try { + es.awaitTermination(5, TimeUnit.MINUTES); + } catch (InterruptedException e) { + errorCount.incrementAndGet(); + throw new RuntimeException(e); + } + return null; + }); + System.out.println("EP sign in " + time); + assert time < 15000; + } + { // Passwordless sign-ups + System.out.println("Measure passwordless sign-ups"); + long time = measureTime(() -> { + ExecutorService es = Executors.newFixedThreadPool(100); + for (int i = 0; i < 500; i++) { + int finalI = i; + es.execute(() -> { + try { + Passwordless.CreateCodeResponse code = Passwordless.createCode(main, + "pl" + finalI + "@example.com", null, null, null); + Passwordless.consumeCode(main, code.deviceId, code.deviceIdHash, code.userInputCode, null); + } catch (Exception e) { + errorCount.incrementAndGet(); + throw new RuntimeException(e); + } + }); + } + es.shutdown(); + try { + es.awaitTermination(5, TimeUnit.MINUTES); + } catch (InterruptedException e) { + errorCount.incrementAndGet(); + throw new RuntimeException(e); + } + return null; + }); + System.out.println("PL sign up " + time); + assert time < 5000; + } + { // Passwordless sign-ins + System.out.println("Measure passwordless sign-ins"); + long time = measureTime(() -> { + ExecutorService es = Executors.newFixedThreadPool(100); + for (int i = 0; i < 500; i++) { + int finalI = i; + es.execute(() -> { + try { + Passwordless.CreateCodeResponse code = Passwordless.createCode(main, + "pl" + finalI + "@example.com", null, null, null); + Passwordless.consumeCode(main, code.deviceId, code.deviceIdHash, code.userInputCode, null); + } catch (Exception e) { + errorCount.incrementAndGet(); + throw new RuntimeException(e); + } + }); + } + es.shutdown(); + try { + es.awaitTermination(5, TimeUnit.MINUTES); + } catch (InterruptedException e) { + errorCount.incrementAndGet(); + throw new RuntimeException(e); + } + return null; + }); + System.out.println("PL sign in " + time); + assert time < 5000; + } + { // Thirdparty sign-ups + System.out.println("Measure thirdparty sign-ups"); + long time = measureTime(() -> { + ExecutorService es = Executors.newFixedThreadPool(100); + for (int i = 0; i < 500; i++) { + int finalI = i; + es.execute(() -> { + try { + ThirdParty.signInUp(main, "twitter", "twitterid" + finalI, "twitter" + finalI + "@example.com"); + } catch (Exception e) { + errorCount.incrementAndGet(); + throw new RuntimeException(e); + } + }); + } + es.shutdown(); + try { + es.awaitTermination(5, TimeUnit.MINUTES); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return null; + }); + System.out.println("Thirdparty sign up " + time); + assert time < 5000; + } + { // Thirdparty sign-ins + System.out.println("Measure thirdparty sign-ins"); + long time = measureTime(() -> { + ExecutorService es = Executors.newFixedThreadPool(100); + for (int i = 0; i < 500; i++) { + int finalI = i; + es.execute(() -> { + try { + ThirdParty.signInUp(main, "twitter", "twitterid" + finalI, "twitter" + finalI + "@example.com"); + } catch (Exception e) { + errorCount.incrementAndGet(); + throw new RuntimeException(e); + } + }); + } + es.shutdown(); + try { + es.awaitTermination(5, TimeUnit.MINUTES); + } catch (InterruptedException e) { + errorCount.incrementAndGet(); + throw new RuntimeException(e); + } + return null; + }); + System.out.println("Thirdparty sign in " + time); + assert time < 5000; + } + { // Measure user pagination + long time = measureTime(() -> { + try { + long count = 0; + UserPaginationContainer users = AuthRecipe.getUsers(main, 500, "ASC", null, null, null); + while (true) { + for (AuthRecipeUserInfo user : users.users) { + count += user.loginMethods.length; + } + if (users.nextPaginationToken == null) { + break; + } + users = AuthRecipe.getUsers(main, 500, "ASC", users.nextPaginationToken, null, null); + if (count >= 500) { + break; + } + } + } catch (Exception e) { + errorCount.incrementAndGet(); + throw new RuntimeException(e); + } + return null; + }); + System.out.println("User pagination " + time); + assert time < 2000; + } + { // Measure update user metadata + long time = measureTime(() -> { + try { + UserPaginationContainer users = AuthRecipe.getUsers(main, 1, "ASC", null, null, null); + UserIdMapping.populateExternalUserIdForUsers( + (StorageLayer.getBaseStorage(main)), + users.users); + + AuthRecipeUserInfo user = users.users[0]; + for (int i = 0; i < 500; i++) { + UserMetadata.updateUserMetadata(main, user.getSupertokensOrExternalUserId(), new JsonObject()); + } + } catch (Exception e) { + errorCount.incrementAndGet(); + throw new RuntimeException(e); + } + return null; + }); + System.out.println("Update user metadata " + time); + } + + assertEquals(0, errorCount.get()); + } + + private static long measureTime(Supplier function) { + long startTime = System.nanoTime(); + + // Call the function + function.get(); + + long endTime = System.nanoTime(); + + // Calculate elapsed time in milliseconds + return (endTime - startTime) / 1000000; // Convert to milliseconds + } +} From 451289b2daaf879bfa99a9c6fb3685e1b657d4a3 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Wed, 13 Mar 2024 13:01:13 +0530 Subject: [PATCH 101/106] fix: pass appId to getUserIdMappingForSuperTokensIds --- .../supertokens/storage/postgresql/Start.java | 4 ++-- .../queries/EmailVerificationQueries.java | 4 ++-- .../queries/UserIdMappingQueries.java | 23 ++++++++++++------- .../postgresql/test/OneMillionUsersTest.java | 3 +++ 4 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index cf611631..4e2ee20e 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -2231,10 +2231,10 @@ public boolean updateOrDeleteExternalUserIdInfo(AppIdentifier appIdentifier, Str } @Override - public HashMap getUserIdMappingForSuperTokensIds(ArrayList userIds) + public HashMap getUserIdMappingForSuperTokensIds(AppIdentifier appIdentifier, ArrayList userIds) throws StorageQueryException { try { - return UserIdMappingQueries.getUserIdMappingWithUserIds(this, userIds); + return UserIdMappingQueries.getUserIdMappingWithUserIds(this, appIdentifier, userIds); } catch (SQLException e) { throw new StorageQueryException(e); } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/EmailVerificationQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/EmailVerificationQueries.java index ff9fc950..6fd00660 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/EmailVerificationQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/EmailVerificationQueries.java @@ -271,7 +271,7 @@ public static List isEmailVerified_transaction(Start start, Connection s // calculating the verified emails HashMap supertokensUserIdToExternalUserIdMap = UserIdMappingQueries.getUserIdMappingWithUserIds_Transaction(start, - sqlCon, supertokensUserIds); + sqlCon, appIdentifier, supertokensUserIds); HashMap externalUserIdToSupertokensUserIdMap = new HashMap<>(); List supertokensOrExternalUserIdsToQuery = new ArrayList<>(); @@ -340,7 +340,7 @@ public static List isEmailVerified(Start start, AppIdentifier appIdentif // We have external user id stored in the email verification table, so we need to fetch the mapped userids for // calculating the verified emails HashMap supertokensUserIdToExternalUserIdMap = UserIdMappingQueries.getUserIdMappingWithUserIds(start, - supertokensUserIds); + appIdentifier, supertokensUserIds); HashMap externalUserIdToSupertokensUserIdMap = new HashMap<>(); List supertokensOrExternalUserIdsToQuery = new ArrayList<>(); for (String userId : supertokensUserIds) { diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/UserIdMappingQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/UserIdMappingQueries.java index 24f4fab7..a2388765 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/UserIdMappingQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/UserIdMappingQueries.java @@ -128,7 +128,8 @@ public static UserIdMapping[] getUserIdMappingWithEitherSuperTokensUserIdOrExter } - public static HashMap getUserIdMappingWithUserIds(Start start, List userIds) + public static HashMap getUserIdMappingWithUserIds(Start start, + AppIdentifier appIdentifier, List userIds) throws SQLException, StorageQueryException { if (userIds.size() == 0) { @@ -137,7 +138,8 @@ public static HashMap getUserIdMappingWithUserIds(Start start, L // No need to filter based on tenantId because the id list is already filtered for a tenant StringBuilder QUERY = new StringBuilder( - "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable() + " WHERE supertokens_user_id IN ("); + "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable() + " WHERE app_id = ? AND " + + "supertokens_user_id IN ("); for (int i = 0; i < userIds.size(); i++) { QUERY.append("?"); if (i != userIds.size() - 1) { @@ -147,9 +149,10 @@ public static HashMap getUserIdMappingWithUserIds(Start start, L } QUERY.append(")"); return execute(start, QUERY.toString(), pst -> { + pst.setString(1, appIdentifier.getAppId()); for (int i = 0; i < userIds.size(); i++) { - // i+1 cause this starts with 1 and not 0 - pst.setString(i + 1, userIds.get(i)); + // i+2 cause this starts with 1 and not 0, and 1 is appId + pst.setString(i + 2, userIds.get(i)); } }, result -> { HashMap userIdMappings = new HashMap<>(); @@ -161,7 +164,9 @@ public static HashMap getUserIdMappingWithUserIds(Start start, L }); } - public static HashMap getUserIdMappingWithUserIds_Transaction(Start start, Connection sqlCon, List userIds) + public static HashMap getUserIdMappingWithUserIds_Transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + List userIds) throws SQLException, StorageQueryException { if (userIds.size() == 0) { @@ -170,7 +175,8 @@ public static HashMap getUserIdMappingWithUserIds_Transaction(St // No need to filter based on tenantId because the id list is already filtered for a tenant StringBuilder QUERY = new StringBuilder( - "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable() + " WHERE supertokens_user_id IN ("); + "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable() + " WHERE app_id = ? AND " + + "supertokens_user_id IN ("); for (int i = 0; i < userIds.size(); i++) { QUERY.append("?"); if (i != userIds.size() - 1) { @@ -180,9 +186,10 @@ public static HashMap getUserIdMappingWithUserIds_Transaction(St } QUERY.append(")"); return execute(sqlCon, QUERY.toString(), pst -> { + pst.setString(1, appIdentifier.getAppId()); for (int i = 0; i < userIds.size(); i++) { - // i+1 cause this starts with 1 and not 0 - pst.setString(i + 1, userIds.get(i)); + // i+2 cause this starts with 1 and not 0, and 1 is appId + pst.setString(i + 2, userIds.get(i)); } }, result -> { HashMap userIdMappings = new HashMap<>(); diff --git a/src/test/java/io/supertokens/storage/postgresql/test/OneMillionUsersTest.java b/src/test/java/io/supertokens/storage/postgresql/test/OneMillionUsersTest.java index 621a2431..5b40b133 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/OneMillionUsersTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/OneMillionUsersTest.java @@ -263,6 +263,7 @@ private void createUserData(Main main) throws Exception { while (true) { UserIdMapping.populateExternalUserIdForUsers( + new AppIdentifier(null, null), (StorageLayer.getBaseStorage(main)), usersResult.users); @@ -389,6 +390,7 @@ private void createSessions(Main main) throws Exception { while (true) { UserIdMapping.populateExternalUserIdForUsers( + new AppIdentifier(null, null), (StorageLayer.getBaseStorage(main)), usersResult.users); @@ -879,6 +881,7 @@ private void measureOperations(Main main) throws Exception { try { UserPaginationContainer users = AuthRecipe.getUsers(main, 1, "ASC", null, null, null); UserIdMapping.populateExternalUserIdForUsers( + new AppIdentifier(null, null), (StorageLayer.getBaseStorage(main)), users.users); From b6f90652d64fe476bc4959437777eba6aa766dfd Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Wed, 13 Mar 2024 17:31:28 +0530 Subject: [PATCH 102/106] fix: one million users test --- .../postgresql/test/OneMillionUsersTest.java | 213 ++++++++---------- 1 file changed, 93 insertions(+), 120 deletions(-) diff --git a/src/test/java/io/supertokens/storage/postgresql/test/OneMillionUsersTest.java b/src/test/java/io/supertokens/storage/postgresql/test/OneMillionUsersTest.java index 5b40b133..1fd1cc1c 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/OneMillionUsersTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/OneMillionUsersTest.java @@ -77,6 +77,12 @@ public void beforeEach() { static int TOTAL_USERS = 1000000; static int NUM_THREADS = 16; + Object lock = new Object(); + Set allUserIds = new HashSet<>(); + Set allPrimaryUserIds = new HashSet<>(); + Map userIdMappings = new HashMap<>(); + Map primaryUserIdMappings = new HashMap<>(); + private void createEmailPasswordUsers(Main main) throws Exception { System.out.println("Creating emailpassword users..."); @@ -103,6 +109,9 @@ private void createEmailPasswordUsers(Main main) throws Exception { storage.signUp(TenantIdentifier.BASE_TENANT, userId, "eptest" + finalI + "@example.com", combinedPasswordHash, timeJoined); + synchronized (lock) { + allUserIds.add(userId); + } } catch (Exception e) { throw new RuntimeException(e); } @@ -129,6 +138,9 @@ private void createPasswordlessUsersWithEmail(Main main) throws Exception { long timeJoined = System.currentTimeMillis(); try { storage.createUser(TenantIdentifier.BASE_TENANT, userId, "pltest" + finalI + "@example.com", null, timeJoined); + synchronized (lock) { + allUserIds.add(userId); + } } catch (Exception e) { throw new RuntimeException(e); } @@ -156,6 +168,9 @@ private void createPasswordlessUsersWithPhone(Main main) throws Exception { long timeJoined = System.currentTimeMillis(); try { storage.createUser(TenantIdentifier.BASE_TENANT, userId, null, "+91987654" + finalI, timeJoined); + synchronized (lock) { + allUserIds.add(userId); + } } catch (Exception e) { throw new RuntimeException(e); } @@ -184,6 +199,9 @@ private void createThirdpartyUsers(Main main) throws Exception { try { storage.signUp(TenantIdentifier.BASE_TENANT, userId, "tptest" + finalI + "@example.com", new LoginMethod.ThirdParty("google", "googleid" + finalI), timeJoined ); + synchronized (lock) { + allUserIds.add(userId); + } } catch (Exception e) { throw new RuntimeException(e); } @@ -211,42 +229,25 @@ private void createUserIdMappings(Main main) throws Exception { System.out.println("Creating user id mappings..."); ExecutorService es = Executors.newFixedThreadPool(NUM_THREADS); - - UserPaginationContainer usersResult = AuthRecipe.getUsers(main, 10000, "ASC", null, - null, null); - AtomicLong usersUpdated = new AtomicLong(0); - while (true) { - for (AuthRecipeUserInfo user : usersResult.users) { - es.execute(() -> { - Random random = new Random(); - - // UserId mapping - for (LoginMethod lm : user.loginMethods) { - String userId = user.getSupertokensUserId(); - - if (random.nextBoolean()) { - userId = "ext" + UUID.randomUUID().toString(); - try { - UserIdMapping.createUserIdMapping(main, lm.getSupertokensUserId(), userId, null, false); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - long count = usersUpdated.incrementAndGet(); - if (count % 10000 == 9999) { - System.out.println("Updated " + (count) + " users"); - } + for (String userId : allUserIds) { + es.execute(() -> { + String extUserId = "ext" + UUID.randomUUID().toString(); + try { + UserIdMapping.createUserIdMapping(main, userId, extUserId, null, false); + synchronized (lock) { + userIdMappings.put(userId, extUserId); } - }); - } - if (usersResult.nextPaginationToken == null) { - break; - } - usersResult = AuthRecipe.getUsers(main, 10000, "ASC", usersResult.nextPaginationToken, - null, null); + } catch (Exception e) { + throw new RuntimeException(e); + } + + long count = usersUpdated.incrementAndGet(); + if (count % 10000 == 9999) { + System.out.println("Updated " + (count) + " users"); + } + }); } es.shutdown(); @@ -258,43 +259,27 @@ private void createUserData(Main main) throws Exception { ExecutorService es = Executors.newFixedThreadPool(NUM_THREADS / 2); - UserPaginationContainer usersResult = AuthRecipe.getUsers(main, 500, "ASC", null, - null, null); - - while (true) { - UserIdMapping.populateExternalUserIdForUsers( - new AppIdentifier(null, null), - (StorageLayer.getBaseStorage(main)), - usersResult.users); - - for (AuthRecipeUserInfo user : usersResult.users) { - es.execute(() -> { - Random random = new Random(); + for (String userId : allPrimaryUserIds) { + es.execute(() -> { + Random random = new Random(); - // User Metadata - JsonObject metadata = new JsonObject(); - metadata.addProperty("random", random.nextDouble()); + // User Metadata + JsonObject metadata = new JsonObject(); + metadata.addProperty("random", random.nextDouble()); - try { - UserMetadata.updateUserMetadata(main, user.getSupertokensOrExternalUserId(), metadata); + try { + UserMetadata.updateUserMetadata(main, userIdMappings.get(userId), metadata); - // User Roles - if (random.nextBoolean()) { - UserRoles.addRoleToUser(main, user.getSupertokensOrExternalUserId(), "admin"); - } else { - UserRoles.addRoleToUser(main, user.getSupertokensOrExternalUserId(), "user"); - } - } catch (Exception e) { - throw new RuntimeException(e); + // User Roles + if (random.nextBoolean()) { + UserRoles.addRoleToUser(main, userIdMappings.get(userId), "admin"); + } else { + UserRoles.addRoleToUser(main, userIdMappings.get(userId), "user"); } - }); - } - - if (usersResult.nextPaginationToken == null) { - break; - } - usersResult = AuthRecipe.getUsers(main, 500, "ASC", usersResult.nextPaginationToken, - null, null); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); } es.shutdown(); @@ -303,25 +288,7 @@ private void createUserData(Main main) throws Exception { private void doAccountLinking(Main main) throws Exception { Set userIds = new HashSet<>(); - - long st = System.currentTimeMillis(); - UserPaginationContainer usersResult = AuthRecipe.getUsers(main, 1000, "ASC", null, - null, null); - - while (true) { - for (AuthRecipeUserInfo user : usersResult.users) { - userIds.add(user.getSupertokensUserId()); - } - if (usersResult.nextPaginationToken == null) { - break; - } - usersResult = AuthRecipe.getUsers(main, 1000, "ASC", usersResult.nextPaginationToken, - null, null); - } - - long en = System.currentTimeMillis(); - - System.out.println("Time taken to get " + TOTAL_USERS + " users (before account linking): " + ((en - st) / 1000) + " sec"); + userIds.addAll(allUserIds); assertEquals(TOTAL_USERS, userIds.size()); @@ -366,6 +333,13 @@ private void doAccountLinking(Main main) throws Exception { throw new RuntimeException(e); } + synchronized (lock) { + allPrimaryUserIds.add(userIdsArray[0]); + for (String userId : userIdsArray) { + primaryUserIdMappings.put(userId, userIdsArray[0]); + } + } + long total = accountsLinked.addAndGet(userIdsArray.length); if (total % 10000 > 9996) { System.out.println("Linked " + (accountsLinked) + " users"); @@ -385,39 +359,24 @@ private void createSessions(Main main) throws Exception { ExecutorService es = Executors.newFixedThreadPool(NUM_THREADS); - UserPaginationContainer usersResult = AuthRecipe.getUsers(main, 500, "ASC", null, - null, null); - - while (true) { - UserIdMapping.populateExternalUserIdForUsers( - new AppIdentifier(null, null), - (StorageLayer.getBaseStorage(main)), - usersResult.users); - - for (AuthRecipeUserInfo user : usersResult.users) { - es.execute(() -> { - try { - for (LoginMethod lM : user.loginMethods) { - String userId = lM.getSupertokensOrExternalUserId(); - SessionInformationHolder session = Session.createNewSession(main, - userId, new JsonObject(), new JsonObject()); - - if (new Random().nextFloat() < 0.05) { - accessToken = session.accessToken.token; - sessionUserId = userId; - } - } + for (String userId : allUserIds) { + String finalUserId = userId; + es.execute(() -> { + try { + SessionInformationHolder session = Session.createNewSession(main, + userIdMappings.get(finalUserId), new JsonObject(), new JsonObject()); - } catch (Exception e) { - throw new RuntimeException(e); + if (new Random().nextFloat() < 0.05) { + synchronized (lock) { + accessToken = session.accessToken.token; + sessionUserId = userIdMappings.get(primaryUserIdMappings.get(finalUserId)); + } } - }); - } - if (usersResult.nextPaginationToken == null) { - break; - } - usersResult = AuthRecipe.getUsers(main, 500, "ASC", usersResult.nextPaginationToken, - null, null); + + } catch (Exception e) { + throw new RuntimeException(e); + } + }); } es.shutdown(); @@ -426,9 +385,9 @@ private void createSessions(Main main) throws Exception { @Test public void testCreatingOneMillionUsers() throws Exception { - if (System.getenv("ONE_MILLION_USERS_TEST") == null) { - return; - } +// if (System.getenv("ONE_MILLION_USERS_TEST") == null) { +// return; +// } String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); @@ -486,8 +445,22 @@ public void testCreatingOneMillionUsers() throws Exception { sanityCheckAPIs(process.getProcess()); Runtime.getRuntime().gc(); + System.gc(); + System.runFinalization(); Thread.sleep(10000); + process.kill(false); + process = TestingProcessManager.start(args, false); + Utils.setValueInConfig("firebase_password_hashing_signer_key", + "gRhC3eDeQOdyEn4bMd9c6kxguWVmcIVq/SKa0JDPFeM6TcEevkaW56sIWfx88OHbJKnCXdWscZx0l2WbCJ1wbg=="); + Utils.setValueInConfig("postgresql_connection_pool_size", "500"); + + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + Thread memoryChecker = new Thread(() -> { while (memoryCheckRunning.get()) { Runtime rt = Runtime.getRuntime(); @@ -514,7 +487,7 @@ public void testCreatingOneMillionUsers() throws Exception { memoryChecker.join(); System.out.println("Max memory used: " + (maxMemory.get() / (1024 * 1024)) + " MB"); - assert maxMemory.get() < 320 * 1024 * 1024; // must be less than 320 mb + assert maxMemory.get() < 256 * 1024 * 1024; // must be less than 320 mb process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); From 8f4b1a9f804ad9af8cc7bd8a706e9999b611924b Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Wed, 13 Mar 2024 17:48:35 +0530 Subject: [PATCH 103/106] fix: versions --- CHANGELOG.md | 2 ++ build.gradle | 2 +- pluginInterfaceSupported.json | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 722bad31..91d99ba2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [7.0.0] + - Replace `TotpNotEnabledError` with `UnknownUserIdTotpError`. - Support for MFA recipe - Adds a new `useStaticKey` param to `updateSessionInfo_Transaction` diff --git a/build.gradle b/build.gradle index baafed34..3d976b54 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'java-library' } -version = "6.0.0" +version = "7.0.0" repositories { mavenCentral() diff --git a/pluginInterfaceSupported.json b/pluginInterfaceSupported.json index e9d4c148..f9d5be77 100644 --- a/pluginInterfaceSupported.json +++ b/pluginInterfaceSupported.json @@ -1,6 +1,6 @@ { "_comment": "contains a list of plugin interfaces branch names that this core supports", "versions": [ - "5.0" + "6.0" ] } \ No newline at end of file From 68cb4924b3135f98959094e0c506f3522c8dd7cd Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Wed, 13 Mar 2024 17:48:58 +0530 Subject: [PATCH 104/106] fix: versions --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91d99ba2..8322cadf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] -## [7.0.0] +## [7.0.0] - 2024-03-13 - Replace `TotpNotEnabledError` with `UnknownUserIdTotpError`. - Support for MFA recipe From ae791e187b0e7fe13c408e9b88323a3a5bbdf31a Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Wed, 13 Mar 2024 18:14:34 +0530 Subject: [PATCH 105/106] Remaining changes (#206) * fix: pass appId to getUserIdMappingForSuperTokensIds * fix: one million users test * fix: versions * fix: versions --- CHANGELOG.md | 2 + build.gradle | 2 +- pluginInterfaceSupported.json | 2 +- .../supertokens/storage/postgresql/Start.java | 4 +- .../queries/EmailVerificationQueries.java | 4 +- .../queries/UserIdMappingQueries.java | 23 +- .../postgresql/test/OneMillionUsersTest.java | 212 ++++++++---------- 7 files changed, 117 insertions(+), 132 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 722bad31..8322cadf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [7.0.0] - 2024-03-13 + - Replace `TotpNotEnabledError` with `UnknownUserIdTotpError`. - Support for MFA recipe - Adds a new `useStaticKey` param to `updateSessionInfo_Transaction` diff --git a/build.gradle b/build.gradle index baafed34..3d976b54 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'java-library' } -version = "6.0.0" +version = "7.0.0" repositories { mavenCentral() diff --git a/pluginInterfaceSupported.json b/pluginInterfaceSupported.json index e9d4c148..f9d5be77 100644 --- a/pluginInterfaceSupported.json +++ b/pluginInterfaceSupported.json @@ -1,6 +1,6 @@ { "_comment": "contains a list of plugin interfaces branch names that this core supports", "versions": [ - "5.0" + "6.0" ] } \ No newline at end of file diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index cf611631..4e2ee20e 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -2231,10 +2231,10 @@ public boolean updateOrDeleteExternalUserIdInfo(AppIdentifier appIdentifier, Str } @Override - public HashMap getUserIdMappingForSuperTokensIds(ArrayList userIds) + public HashMap getUserIdMappingForSuperTokensIds(AppIdentifier appIdentifier, ArrayList userIds) throws StorageQueryException { try { - return UserIdMappingQueries.getUserIdMappingWithUserIds(this, userIds); + return UserIdMappingQueries.getUserIdMappingWithUserIds(this, appIdentifier, userIds); } catch (SQLException e) { throw new StorageQueryException(e); } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/EmailVerificationQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/EmailVerificationQueries.java index ff9fc950..6fd00660 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/EmailVerificationQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/EmailVerificationQueries.java @@ -271,7 +271,7 @@ public static List isEmailVerified_transaction(Start start, Connection s // calculating the verified emails HashMap supertokensUserIdToExternalUserIdMap = UserIdMappingQueries.getUserIdMappingWithUserIds_Transaction(start, - sqlCon, supertokensUserIds); + sqlCon, appIdentifier, supertokensUserIds); HashMap externalUserIdToSupertokensUserIdMap = new HashMap<>(); List supertokensOrExternalUserIdsToQuery = new ArrayList<>(); @@ -340,7 +340,7 @@ public static List isEmailVerified(Start start, AppIdentifier appIdentif // We have external user id stored in the email verification table, so we need to fetch the mapped userids for // calculating the verified emails HashMap supertokensUserIdToExternalUserIdMap = UserIdMappingQueries.getUserIdMappingWithUserIds(start, - supertokensUserIds); + appIdentifier, supertokensUserIds); HashMap externalUserIdToSupertokensUserIdMap = new HashMap<>(); List supertokensOrExternalUserIdsToQuery = new ArrayList<>(); for (String userId : supertokensUserIds) { diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/UserIdMappingQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/UserIdMappingQueries.java index 24f4fab7..a2388765 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/UserIdMappingQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/UserIdMappingQueries.java @@ -128,7 +128,8 @@ public static UserIdMapping[] getUserIdMappingWithEitherSuperTokensUserIdOrExter } - public static HashMap getUserIdMappingWithUserIds(Start start, List userIds) + public static HashMap getUserIdMappingWithUserIds(Start start, + AppIdentifier appIdentifier, List userIds) throws SQLException, StorageQueryException { if (userIds.size() == 0) { @@ -137,7 +138,8 @@ public static HashMap getUserIdMappingWithUserIds(Start start, L // No need to filter based on tenantId because the id list is already filtered for a tenant StringBuilder QUERY = new StringBuilder( - "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable() + " WHERE supertokens_user_id IN ("); + "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable() + " WHERE app_id = ? AND " + + "supertokens_user_id IN ("); for (int i = 0; i < userIds.size(); i++) { QUERY.append("?"); if (i != userIds.size() - 1) { @@ -147,9 +149,10 @@ public static HashMap getUserIdMappingWithUserIds(Start start, L } QUERY.append(")"); return execute(start, QUERY.toString(), pst -> { + pst.setString(1, appIdentifier.getAppId()); for (int i = 0; i < userIds.size(); i++) { - // i+1 cause this starts with 1 and not 0 - pst.setString(i + 1, userIds.get(i)); + // i+2 cause this starts with 1 and not 0, and 1 is appId + pst.setString(i + 2, userIds.get(i)); } }, result -> { HashMap userIdMappings = new HashMap<>(); @@ -161,7 +164,9 @@ public static HashMap getUserIdMappingWithUserIds(Start start, L }); } - public static HashMap getUserIdMappingWithUserIds_Transaction(Start start, Connection sqlCon, List userIds) + public static HashMap getUserIdMappingWithUserIds_Transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + List userIds) throws SQLException, StorageQueryException { if (userIds.size() == 0) { @@ -170,7 +175,8 @@ public static HashMap getUserIdMappingWithUserIds_Transaction(St // No need to filter based on tenantId because the id list is already filtered for a tenant StringBuilder QUERY = new StringBuilder( - "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable() + " WHERE supertokens_user_id IN ("); + "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable() + " WHERE app_id = ? AND " + + "supertokens_user_id IN ("); for (int i = 0; i < userIds.size(); i++) { QUERY.append("?"); if (i != userIds.size() - 1) { @@ -180,9 +186,10 @@ public static HashMap getUserIdMappingWithUserIds_Transaction(St } QUERY.append(")"); return execute(sqlCon, QUERY.toString(), pst -> { + pst.setString(1, appIdentifier.getAppId()); for (int i = 0; i < userIds.size(); i++) { - // i+1 cause this starts with 1 and not 0 - pst.setString(i + 1, userIds.get(i)); + // i+2 cause this starts with 1 and not 0, and 1 is appId + pst.setString(i + 2, userIds.get(i)); } }, result -> { HashMap userIdMappings = new HashMap<>(); diff --git a/src/test/java/io/supertokens/storage/postgresql/test/OneMillionUsersTest.java b/src/test/java/io/supertokens/storage/postgresql/test/OneMillionUsersTest.java index 621a2431..1fd1cc1c 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/OneMillionUsersTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/OneMillionUsersTest.java @@ -77,6 +77,12 @@ public void beforeEach() { static int TOTAL_USERS = 1000000; static int NUM_THREADS = 16; + Object lock = new Object(); + Set allUserIds = new HashSet<>(); + Set allPrimaryUserIds = new HashSet<>(); + Map userIdMappings = new HashMap<>(); + Map primaryUserIdMappings = new HashMap<>(); + private void createEmailPasswordUsers(Main main) throws Exception { System.out.println("Creating emailpassword users..."); @@ -103,6 +109,9 @@ private void createEmailPasswordUsers(Main main) throws Exception { storage.signUp(TenantIdentifier.BASE_TENANT, userId, "eptest" + finalI + "@example.com", combinedPasswordHash, timeJoined); + synchronized (lock) { + allUserIds.add(userId); + } } catch (Exception e) { throw new RuntimeException(e); } @@ -129,6 +138,9 @@ private void createPasswordlessUsersWithEmail(Main main) throws Exception { long timeJoined = System.currentTimeMillis(); try { storage.createUser(TenantIdentifier.BASE_TENANT, userId, "pltest" + finalI + "@example.com", null, timeJoined); + synchronized (lock) { + allUserIds.add(userId); + } } catch (Exception e) { throw new RuntimeException(e); } @@ -156,6 +168,9 @@ private void createPasswordlessUsersWithPhone(Main main) throws Exception { long timeJoined = System.currentTimeMillis(); try { storage.createUser(TenantIdentifier.BASE_TENANT, userId, null, "+91987654" + finalI, timeJoined); + synchronized (lock) { + allUserIds.add(userId); + } } catch (Exception e) { throw new RuntimeException(e); } @@ -184,6 +199,9 @@ private void createThirdpartyUsers(Main main) throws Exception { try { storage.signUp(TenantIdentifier.BASE_TENANT, userId, "tptest" + finalI + "@example.com", new LoginMethod.ThirdParty("google", "googleid" + finalI), timeJoined ); + synchronized (lock) { + allUserIds.add(userId); + } } catch (Exception e) { throw new RuntimeException(e); } @@ -211,42 +229,25 @@ private void createUserIdMappings(Main main) throws Exception { System.out.println("Creating user id mappings..."); ExecutorService es = Executors.newFixedThreadPool(NUM_THREADS); - - UserPaginationContainer usersResult = AuthRecipe.getUsers(main, 10000, "ASC", null, - null, null); - AtomicLong usersUpdated = new AtomicLong(0); - while (true) { - for (AuthRecipeUserInfo user : usersResult.users) { - es.execute(() -> { - Random random = new Random(); - - // UserId mapping - for (LoginMethod lm : user.loginMethods) { - String userId = user.getSupertokensUserId(); - - if (random.nextBoolean()) { - userId = "ext" + UUID.randomUUID().toString(); - try { - UserIdMapping.createUserIdMapping(main, lm.getSupertokensUserId(), userId, null, false); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - long count = usersUpdated.incrementAndGet(); - if (count % 10000 == 9999) { - System.out.println("Updated " + (count) + " users"); - } + for (String userId : allUserIds) { + es.execute(() -> { + String extUserId = "ext" + UUID.randomUUID().toString(); + try { + UserIdMapping.createUserIdMapping(main, userId, extUserId, null, false); + synchronized (lock) { + userIdMappings.put(userId, extUserId); } - }); - } - if (usersResult.nextPaginationToken == null) { - break; - } - usersResult = AuthRecipe.getUsers(main, 10000, "ASC", usersResult.nextPaginationToken, - null, null); + } catch (Exception e) { + throw new RuntimeException(e); + } + + long count = usersUpdated.incrementAndGet(); + if (count % 10000 == 9999) { + System.out.println("Updated " + (count) + " users"); + } + }); } es.shutdown(); @@ -258,42 +259,27 @@ private void createUserData(Main main) throws Exception { ExecutorService es = Executors.newFixedThreadPool(NUM_THREADS / 2); - UserPaginationContainer usersResult = AuthRecipe.getUsers(main, 500, "ASC", null, - null, null); - - while (true) { - UserIdMapping.populateExternalUserIdForUsers( - (StorageLayer.getBaseStorage(main)), - usersResult.users); - - for (AuthRecipeUserInfo user : usersResult.users) { - es.execute(() -> { - Random random = new Random(); + for (String userId : allPrimaryUserIds) { + es.execute(() -> { + Random random = new Random(); - // User Metadata - JsonObject metadata = new JsonObject(); - metadata.addProperty("random", random.nextDouble()); + // User Metadata + JsonObject metadata = new JsonObject(); + metadata.addProperty("random", random.nextDouble()); - try { - UserMetadata.updateUserMetadata(main, user.getSupertokensOrExternalUserId(), metadata); + try { + UserMetadata.updateUserMetadata(main, userIdMappings.get(userId), metadata); - // User Roles - if (random.nextBoolean()) { - UserRoles.addRoleToUser(main, user.getSupertokensOrExternalUserId(), "admin"); - } else { - UserRoles.addRoleToUser(main, user.getSupertokensOrExternalUserId(), "user"); - } - } catch (Exception e) { - throw new RuntimeException(e); + // User Roles + if (random.nextBoolean()) { + UserRoles.addRoleToUser(main, userIdMappings.get(userId), "admin"); + } else { + UserRoles.addRoleToUser(main, userIdMappings.get(userId), "user"); } - }); - } - - if (usersResult.nextPaginationToken == null) { - break; - } - usersResult = AuthRecipe.getUsers(main, 500, "ASC", usersResult.nextPaginationToken, - null, null); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); } es.shutdown(); @@ -302,25 +288,7 @@ private void createUserData(Main main) throws Exception { private void doAccountLinking(Main main) throws Exception { Set userIds = new HashSet<>(); - - long st = System.currentTimeMillis(); - UserPaginationContainer usersResult = AuthRecipe.getUsers(main, 1000, "ASC", null, - null, null); - - while (true) { - for (AuthRecipeUserInfo user : usersResult.users) { - userIds.add(user.getSupertokensUserId()); - } - if (usersResult.nextPaginationToken == null) { - break; - } - usersResult = AuthRecipe.getUsers(main, 1000, "ASC", usersResult.nextPaginationToken, - null, null); - } - - long en = System.currentTimeMillis(); - - System.out.println("Time taken to get " + TOTAL_USERS + " users (before account linking): " + ((en - st) / 1000) + " sec"); + userIds.addAll(allUserIds); assertEquals(TOTAL_USERS, userIds.size()); @@ -365,6 +333,13 @@ private void doAccountLinking(Main main) throws Exception { throw new RuntimeException(e); } + synchronized (lock) { + allPrimaryUserIds.add(userIdsArray[0]); + for (String userId : userIdsArray) { + primaryUserIdMappings.put(userId, userIdsArray[0]); + } + } + long total = accountsLinked.addAndGet(userIdsArray.length); if (total % 10000 > 9996) { System.out.println("Linked " + (accountsLinked) + " users"); @@ -384,38 +359,24 @@ private void createSessions(Main main) throws Exception { ExecutorService es = Executors.newFixedThreadPool(NUM_THREADS); - UserPaginationContainer usersResult = AuthRecipe.getUsers(main, 500, "ASC", null, - null, null); - - while (true) { - UserIdMapping.populateExternalUserIdForUsers( - (StorageLayer.getBaseStorage(main)), - usersResult.users); - - for (AuthRecipeUserInfo user : usersResult.users) { - es.execute(() -> { - try { - for (LoginMethod lM : user.loginMethods) { - String userId = lM.getSupertokensOrExternalUserId(); - SessionInformationHolder session = Session.createNewSession(main, - userId, new JsonObject(), new JsonObject()); - - if (new Random().nextFloat() < 0.05) { - accessToken = session.accessToken.token; - sessionUserId = userId; - } - } + for (String userId : allUserIds) { + String finalUserId = userId; + es.execute(() -> { + try { + SessionInformationHolder session = Session.createNewSession(main, + userIdMappings.get(finalUserId), new JsonObject(), new JsonObject()); - } catch (Exception e) { - throw new RuntimeException(e); + if (new Random().nextFloat() < 0.05) { + synchronized (lock) { + accessToken = session.accessToken.token; + sessionUserId = userIdMappings.get(primaryUserIdMappings.get(finalUserId)); + } } - }); - } - if (usersResult.nextPaginationToken == null) { - break; - } - usersResult = AuthRecipe.getUsers(main, 500, "ASC", usersResult.nextPaginationToken, - null, null); + + } catch (Exception e) { + throw new RuntimeException(e); + } + }); } es.shutdown(); @@ -424,9 +385,9 @@ private void createSessions(Main main) throws Exception { @Test public void testCreatingOneMillionUsers() throws Exception { - if (System.getenv("ONE_MILLION_USERS_TEST") == null) { - return; - } +// if (System.getenv("ONE_MILLION_USERS_TEST") == null) { +// return; +// } String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); @@ -484,8 +445,22 @@ public void testCreatingOneMillionUsers() throws Exception { sanityCheckAPIs(process.getProcess()); Runtime.getRuntime().gc(); + System.gc(); + System.runFinalization(); Thread.sleep(10000); + process.kill(false); + process = TestingProcessManager.start(args, false); + Utils.setValueInConfig("firebase_password_hashing_signer_key", + "gRhC3eDeQOdyEn4bMd9c6kxguWVmcIVq/SKa0JDPFeM6TcEevkaW56sIWfx88OHbJKnCXdWscZx0l2WbCJ1wbg=="); + Utils.setValueInConfig("postgresql_connection_pool_size", "500"); + + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + Thread memoryChecker = new Thread(() -> { while (memoryCheckRunning.get()) { Runtime rt = Runtime.getRuntime(); @@ -512,7 +487,7 @@ public void testCreatingOneMillionUsers() throws Exception { memoryChecker.join(); System.out.println("Max memory used: " + (maxMemory.get() / (1024 * 1024)) + " MB"); - assert maxMemory.get() < 320 * 1024 * 1024; // must be less than 320 mb + assert maxMemory.get() < 256 * 1024 * 1024; // must be less than 320 mb process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -879,6 +854,7 @@ private void measureOperations(Main main) throws Exception { try { UserPaginationContainer users = AuthRecipe.getUsers(main, 1, "ASC", null, null, null); UserIdMapping.populateExternalUserIdForUsers( + new AppIdentifier(null, null), (StorageLayer.getBaseStorage(main)), users.users); From c91dea71c8d3f7cc84f336f0ba2aafc8ce1449c4 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Wed, 13 Mar 2024 20:20:10 +0530 Subject: [PATCH 106/106] fix: one million users --- .../postgresql/test/OneMillionUsersTest.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/test/java/io/supertokens/storage/postgresql/test/OneMillionUsersTest.java b/src/test/java/io/supertokens/storage/postgresql/test/OneMillionUsersTest.java index 1fd1cc1c..5f002be2 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/OneMillionUsersTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/OneMillionUsersTest.java @@ -385,9 +385,9 @@ private void createSessions(Main main) throws Exception { @Test public void testCreatingOneMillionUsers() throws Exception { -// if (System.getenv("ONE_MILLION_USERS_TEST") == null) { -// return; -// } + if (System.getenv("ONE_MILLION_USERS_TEST") == null) { + return; + } String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); @@ -443,13 +443,18 @@ public void testCreatingOneMillionUsers() throws Exception { } sanityCheckAPIs(process.getProcess()); + allUserIds.clear(); + allPrimaryUserIds.clear(); + userIdMappings.clear(); + primaryUserIdMappings.clear(); + + process.kill(false); Runtime.getRuntime().gc(); System.gc(); System.runFinalization(); Thread.sleep(10000); - - process.kill(false); + process = TestingProcessManager.start(args, false); Utils.setValueInConfig("firebase_password_hashing_signer_key", "gRhC3eDeQOdyEn4bMd9c6kxguWVmcIVq/SKa0JDPFeM6TcEevkaW56sIWfx88OHbJKnCXdWscZx0l2WbCJ1wbg=="); @@ -487,7 +492,7 @@ public void testCreatingOneMillionUsers() throws Exception { memoryChecker.join(); System.out.println("Max memory used: " + (maxMemory.get() / (1024 * 1024)) + " MB"); - assert maxMemory.get() < 256 * 1024 * 1024; // must be less than 320 mb + assert maxMemory.get() < 256 * 1024 * 1024; // must be less than 256 mb process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));