From f99cd8f3e29caf194352a1a9c4b38b0f3e6d02b4 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Thu, 19 Jan 2023 14:02:18 +0530 Subject: [PATCH 001/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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 zcmY&<18^o=*L7?sPwY%=+qP{xnb>(^+qP}nn%K5E;bbz&Klk3h>Z`Y_PxtQK>vW&0 zvumC1U2Cso!7Q~wBPz*)L%@N6z`%eg1gIw>Qh@(+;*g<`4^Y3Gneq|OEb0+Rms$!zJMQJ~Dpv7ksm2I&p6 zX2_&8s)*7Sqen35(xT8(EvY43JTMAtn&l|jCiHg1?vCz@2qFStNZj65eXXi-0#5l} z&I9tBVNTrhwmfbU1?$Pp@NMteZRZ*HoL8sE*XLclAn3%Kgcz0{*fyF%VfI0q#YpXC zna$>ysCxThntg_hcB~_Anmz|m{i<7ruUbc9g|3Q|ZJML?nZ`Qil&x1!*uQ3%cn$0{ZLh_e)w2cX;(TK00YXv=$inMlQti05VpSMr^OLlWTVZk z+XyT3rg6lhi{N+Rhu~m-E zF`D%84^!vaNSs3Q0mQ_IT)}p^s?xBLyN<;|Nm~@#C z>r=SO0Y#eZ74mWsJ$wX^Sx->UDV~C;vWveyJ!ubXMP8THm677R_E30+8JGDAuF)+> zi*5~rWjflVo&$Y){v4$_Bm1NzTG>5wux9hmDsxpn?DZ-sBh6n6Oh1%h2#`yui4j?x zI|hm=Wiv+Eqw-W}9?o0G8G5FO&67Jzc{jwIZ`_?jN#+eC5z5hlVG62QIqLbLIv()PrMyoDUO`xYa#gus5BqZ1=q0P>l$Mw&z3O9O7trc8LACN0X;Pi zL+I3^1P!{a$5*MZ;X z&|pspCP?`~NL-R|qyvmDNoNFJpbp+kj8L`Ttjs|fvOxWSi3AN3gdwQ}p1|<8nT0;# zJHkbf9dC!G?PwRNq;D%P`xJkNG-@fea+4Y7XT;VN+b8rL@2;#Ecwi^L85}>8zcu!q z=N+uCyyVQ839JQvM0f;ps5`rcg+U+C% zmm@|jYvLIY#ltC4r^40#TvM!*uO<3BmVwbB_u<0$eR~c6pUSJ1WX-Pzzxp5yN@6)a zvSU$`9&J!TkqQ zj4;lEFHDVS1bc&&-5D1{JC=xyHV^qFG5UqwzEy$33f*`a!ESN_-N9g9DNZlVpx!%f zD!<4ztgg#8AT%CQi!Vv7L0$*zS%~_|oKnYHufV^+|A%hUcdS5JlNq7Vk^W&BZ<-aT zNkkA3E|O#=C>h`h0B9)Hl+7fx{>>;L;RNnmNR3kBZ6Ug@yw*&5bn zA-mc(8{M0e4e#aZ)|IxF*xC_YTV$(N58J!neRtDsHeZew?kpR=$`^C_&5ylz1YfUx z9=_;#-k%5Kkyg1G%F^U))FP#Dsmh%NRl}F4?1{>;pCdr8-tg8GvI^f`hx1tp;p8+n zf{Fqvb+HPS!pRaCo#Jd*-uxxHar|WjUSr;}(ihvv9BbY`X%>bl(j{`mig${xUMaWy z1iJb`1zpNzCF3c4McQo+xItpPcv)5RY;B87X-gn<-^pFdGNm`)M{yq&G^r$3g33ZY zq3>y^R+)j9s_mv15??%+73x+{w8-$sa?=OBtQb|a8foq9(XSS?Y~im}!_HOqMC`R} zcXR3$fvszh$r%_rL>mMtUAad)h2q z3X-}OEHN7yHLPHpIYZZ5YpN{ai1ih(Nh>Wv)0%+GQ?RAZFAJ_{cv;C$NZnGyETKBu zqPC4t_*J{*TxoMv=`F2t=9GDK zQ5yGcLRTA%{Ir<%o0GX^ua@G)m1))Tre7_dFl6nOtjJ`4;8U8HLxCiCanLT0@wxqc z`k@Q-CbuuON8PNON8*u8IHag5ZT{-VyQ4iw78N5nU8T}$=RWGvB;wZ;>KLR4%O;vAUC){c* zWl*lQ=E@`k4WS;_HEm%~IS*+^0mtg|Gm9OFQA(&5o;8^v*}d(VSB-eHVk9K`;%I~r zj?tBngd*I6ug+=Y=)S(Y3c7L$h4w?rx&c>|QWbDniZM%8jbwAqG?Dq}W(nfhR*Z|p z#-@Nj!bB|-v5^xw&#hr#TK*2eH7Y0duPAoK4xGuLW*?Rh*Jgn;pS;)Vn%CU^+?WOO za)&a|EgOO!%Nh#MeXSKl$qFaI4YPW&I2s;W97wBNTP`Mq{7nr8fEf7~|A z*riY7SVb=;2i@*)7L2;2d5J07Uc6ah#l&;`;qrF#6>2@ILS2hZu`cAw@yxe2S1)JZ zOi;SSXr}?6h>DJ-wDL${z`IWmK#a<g17-Zs&{VC6=|Qm912<+`q}3T72j|*10S;H^ zl=e{fU8zxJIp(Ig+9s6e$kk3xyCouA*DH zb1yb&TcvLlQ{aaIHWxiFk);(r8QNLkF3rX(dPCd5q+90g#P21}Ceg>f^S;1KI;D0OMTP)`>>L8H{&dTL>aK<@B@}*?%O_K=%M$$J#@%u7f@o-= zbsPN&n)atil!sjoMvN+)t_dN&L^=?T0_l`1wCd-C~ho zfIi-yTjeX5b-Wh}j!Gc~cC-j#CUe1vUo~((rC~lZtyVxR9-|_&rJFaImBtofte!Oz z>)%r2Qx2?-r7Z+q@KqfdI@&P#q>S~KTfY&P7C$!E*zqpQ&l&9dnXNJ;W2HN?;mocu zmt4Qcs<=d41!T6Br$#r)C^L|vqK+1z1A;K)hE`(9r11NLdbX~S*h9ktaJ#9u8~h{` zFBDu&LVSS6;nql}E=eTZiTizpITXfsiFyy?)$38w2JN?2S4Ac)mkMls>pR>uWd-n& zB@t29AB$1zR5w(vwzg22RU4?N=q=~w+}Nn>4X=kaZzc~J)`S!?ut=sndCAg)kVmpw zsYkEVZ}bO(Y%#^772f#vVIlPjL!6Q#wh8m6vR1(8Z!P3#n<}ui>LR)fg}LN~wTbf_ zGFJ9SR*w(5r*(ZnD@*%>YwQFz{sCexldw^2n(BE4)ISa()E~OS_AGvyVhUdVLOmM? z^Jn{697#m~A$4ym?qYWWtFwZy>l3ufYP{sfkWzox8T|(Tog-C+zwi8rYYI|LG?puk zB1;xXuSJg12WW}WIPA>QSm4n?9^bs*L)w%$TjXwm)|o9=c*kXj5wR%Z;lopF{!ZTssp)Fcj_?fA28t+!$uD=<$6*kEvE5>#nP7M8yus7~ocoY?}#P-3S8zGIwXkE!|4r7);3izx!+ zOqIzihD6=OEv&QL>9~apFbFd|b%YwKQe~SP=4B0OzbnCO>pRF0n2{@{Is+p*$mSD~ zG(N(vK_*#ZfO?lzs!H#Iv3#o?6oug>(JNJB$%iM!D;ZM$&YC68q^9aj`Qv3@Ep#7& zFyEm2o$HQ>k!NHYwS^WXj@C)aaU>Dgi*wM)1Hz1cp$gJyMHj)%CD6C{o0t&9gW7+~ z$n2C_=f+qngek)}#6{O+6^6Szo!_5#G)-OyRrjJy3!;8Fh>YEZP34DL&cs?@k|K>{Ai7)&PX2~p) zjXF&K1Y=)59;A~USf_n&%=~Bh)mU>P+?wrzdsDB1eemKBt9ACa+BQXgihEZ+^w9L5 z161p{Uh9xL8q6;_DL%A_FH)4dSJ`TYmsO;U`2267wgLhojzkUym$XknQgj#AGZ!uX zP;7og#(tC(RE)PNZrpxXXu{tF%*1&H++(0qDizJ-4~d@MQqN{9q%k@iW^d@5$XNs=GRt2_WFNA8j%7?Yy} zY0(|+=~h{V&`Fj7>cjy9XkS2%Ny73sg3Fs8Y+=o*=5n{ z%fzb?qV7-rV?alPs?`0>-;N&JP|vFa&l*EtKMzE4eiqKS6xLW$*@9DD@l(Zv$jCCs zAO2>o z6a`$O$~r>k-b2bA9$^iQhWzHS2K`7S;=>&2OnAjdfZS~Iu1a#(Vm>zI-i_IqQI8es z*X5~)Aogfj5N^7NnO{Y@55Me@c2sF9TpdFQYip`RVDdbWpCMnT-B`Y~p}wMni^RTT z*vv!A2v@k~NBz!;x*hNniMS}j9o2so{GT5|4FagM*Gwo75P1|35cYplQ_bfXt5E-B zp_&Kr;2{3VN;S_C%|iZb2n6IqGg*mJ9H{4Iq_(mm zcsBdZk;My^gk&JZJ^@4Yhk{~z z^&|kH>jTC{-%r+0R5tm?K?6H!j-HDh0&Np0iEb!9c3?qA8`K|#w(+2#p>NonU0^$T z#LP(u_M2w;{iFmELsXCw)kcAQA)Uc4IvY1X9h>dlTHTj2hF3ODNGi}!JQjAMA*J_8 zApZchH`uaJN=9G&SdM9%+~ejg@m+7!*Ciw`^b0 zlH4t;t7ia3ND2OE5f1#lTL>B%6$7v|azVZ!O@o8&?>#rSaZNP8f)iE`;KFXVlG}Y) z2#aNH=DnrdY(X@#O0X<2!rdbldV4xSdGfSn$@^aEyz-kkg9mkUm&J59stH27GoftL0fn= zF78Qq`B}tP`1*=qxlPnFKTbN>k&O*hg3wr)10rix3x|B$6fi3rhg#*|TiKL2u#M|* zFb5~$)T$b=tAJA5`)%CGIm543kWa271k^c^G*Z3QOH= zhoV!n7#5JN3Um1I+NdE5_bPQ7wqctPrxrMtI&NQef2_uc2jN*IQLS`;gCQK7TDYry zjHy36`t~Z-E}x7pa)r6Ag7TqD+P`}|Z^#}ZCufxgC(V-464|+^^|>}m+#R!RY41)n z0GpnS@j%M=DGdVvbg`YYhbJtrHpfU80T0G{K~Q!lvau>Bsw^7z?(*ov08|POBuC}G zaR4`);+gqt+wY-@9-24oB=$T<;34(*GB|&WF^Lv8o}D}>OX7ynGskES6BGs20^Z6X zx)L)4Mt$|PcJa_!I!A;_ctllxA#1571b9B#P++rmWRNF0R91^YjMsF5` z@*4!UeHi?K44}>=hCH}AKG!oAZtpOXH7Oh0a`;N^jYAZFRD2Ywy}vfl{j1GNuq*J- zIgl3yFp0-FL^T!Zp!PRm%a-(w-tgwUU)mu|7o$TxbW1CnL@JsIzJ4LZQ&-GtDB2|) z&Lgac^4NxO({7>b&SK}TNGpdg%SuCebFX;l^{^c!S*@F+TUQ^|>$U1T(Ul;FL9pi# zw3;WA@xVOH$>CLX4<4b$0V>7qSvRiJFc2z#O?_>rbN2+9cdVF(FKvQPy|0Z5Bfk1R zIOKpY%`PAAcjk47nAxVjt~nDzyJ9G~oB%g!C)CDn+YHV+ip1{_I7OCCRPR#VD>xqN zGj2Bs#bH*s52;Yg%qtX3)-3h)P6~=CX(zDq0T&v4gq7V`0mXG}#sNhPPvy{j7GTQH zLg7T;A+GlgKAjm1Pwh}m>;bKvZ_X`)CwH#XA!vh-tN;U3reBe7WS z+hJ=LSTsGO1!0oGaIBxDaQ!A@N9fvIX;1oqUD7|0PGs)I1NEn z@vfxZIw);4QRXUFq?!(^dVc-GlDm2htEKCW&Qc^4!p*JzwL@21DJ#lCuACw3^q9=q zLHKK&EcB|01X7M-My#UFp~kUc^Qy|;w&sOgV++?Oxp?yOaDS&1dME8ZHXptav-5+V z!8#X{GkZKl8>TsK#@a;<5m+FP9718CS|-o6aPiQ>)g6o{WmVlhy|VbjK}1O;wS9~j zPsIs>V1gOT>9-T_uP@-7GT*Y&KnfxSer*xtBg!J!doqvn&?9>74DsB)NVu|q$2YHA zqWx>ha%Ftkzb(8$S#mhJ%GZF=9##M!NCvHt6QBV}S>*|)P}IQ+gH$YX`9Qf&LZmUB9@yAOfZe!WhF5RcCk@H_qAwTd zQ@m_8#pk%2nttma0GEbP6RZz-WhNmD94aK=?6dp_k(r`F;F=Cq?;M%LdKA-hQx zPpw%^H^+Tn&lH$XblWL@3T{WX9 zp_gcoaIX9gq(If=P|r*l;$~no=KM1=7pB}1N)(>_vCt9AIin4uTU0wmgOH=TZbE=h zAMdBAy$;1ILUF7KY0j?p&0Lf)+ZrKnJay<7%yfK{id4qy*kNeRn>b80Tpv#BSffsQ zSei!FsHI}n=?_m>>_sPDgTlj9Pk>ieb{Us67~r`On9kxHq<>d0HEw&a3ujP$=yr{= z@khEsXJ;Q_xw8!_^TiwBIB-Xd-MftXRYm#Khv+D#<=4VU_4DK)WJve8u?kZ-?U(0X z>4Cy_>ERDg_{wsxirg0m_aIJ%7GX!#2lpOP-o1^Uom|vl zt(ZY_;DsmFZtXrljX(0Ml0)Qr!fFTy++>`O4?$HA`o?Xa(n}IP%^&|Wju#!)U3e&s zPhkL&$%$(B2}xY~Z&bjmbEacK&6p9P>cj6huGa3ZU?;~iS7vEr<5hl|+u=VgyP&Vy zIw1|T>uX;t(mlBy8jdN|sv0LQoVsh+nOup__ok@&{c4ORyVlf~thBJHva*%u*c8^NBuR@f;=Z!Oxd*o! zAafO;aGp7dnPl<6I*6o1$hkZu%4*+r% zNvMsTyu8JF>|NCx;n57-fnJY1c*Q<645iqpV*L2Z!IRoMZx=UdnBw*ZQ=n|%s~47k zP`}1KCw{?DB3;=h)4?58#zUL+!Sfpg5Lkam^p=h8|3x@80W7lMddRfy{}XB((@!cV ze+%i@j3YSQPy2=p5Il=z?J-2!Z>Sl6a{U1ND&6l_|3K>(nSeLFO$U4X1=>$unwYZ=g&tB7T&Co-rN?aW**x2s;$Xc=JV-~S+0MNkz>gPR`t9@15;fVi;YSnLQd zIAG$#LGk=BqvRI0d@Dn0} zAwIH}VeTk|X$bJ0Qt&Y?P*ICs9a7vCpbsU))nXtND=fog$DLi>3M0)#Whj!+N;_>* zVxtz|#b5X&kZfA1jVI>u-V57-HSobam`VAK(IwJSz~rr@@%z+`o254{Zc6GTmjK2K%#5EC z>&tz$v?;Khqx9Dwj&`+}XfYrDi|^b5$+a<*v;rJ|GRs-r+0ms0+;Qk1SmIu;)8-lA zX)zkrQ0dLaB;ZDX$2#tJ79IogSL^#vs;FXiw7mv2SdCfx58>!3P3ltjQNH-nDk_M^S1L z;MmNBYEp)a?iV;^ZrI1W13{t{7}jFlY@L{h1@VmBlMw@OsBCXT_S9SyYka*CJdqrY zGgn2}_e|Q!5j6=&#e+Su&{fefk}ZgAJcb%yFKNNag49VT{qqc|Np6AJLj`6Y^{V27 z8)h{kC*)f*YE>MulhyOKQINtn;H6@cAg$Bu49qZD5BD^sBkv!rJ$EGYg<^rLS;{q9 zY}ec;OT`o*f41+=r^Xr@=UHXWx*2Qi1-1NYCzWy6+qrlkwQb`Z*0JMFw~hWFVqe=p zgPK#TwjNnuX@sr16#ANSOEiqL4`*^+7idfWoi^znxN@(HYD=W6NbChmd3yiPqKb>H ztr#3gM zF*QBJBZ$5hl@*LvTw!C6F%a_aW6Rn4(L3kTb#EiK-fj^_Gm*a-l0 z@#k%sx3*-tpHtGd2gT|;4=tuxbbTAUda9Oa)%WO)y;u}{EiXoOH0I6JiEbFvq=J&HVFIlhLf<47Tu&-%rKX7;Ku6Q~^9ydn0m5H8HuohtuB~ggtEx~3 ztd*NhVii)3u%6$|A`%*v0s{dbbrPJ&0@+s#fE5`nf2bUy{f?=Cz}iyZiZ$viE`pJj~{;6WyUG`vcBm(Vvgc^9>K522Y&C(S!tU8>?r z|Me9y6>D>-n8uAZK*ajTrHV^3#~y7i+1iy){ki%QA5uJ2!*YNo|LPLoYD&>_D!OW& z!QCv9fZ~B=Xygr~d=i$snc`AC@0As(o+B=_bbmzmz$0={a3KynlSBGNL2y2=qpfL= z=18Jf8@+6vBG#K1KPbfe%{!eWGrk6EX`F(*&A8*SvN(aSwsCXSO220nmsQX``Z6(R z* z+dq|-1itE?Z)hpRilWw?uHu(EjstU_yb#mT?}RIO&y7yPdGP2W{*fz0nYxbgz#WN` zk#gx5i%l^^iZbvO1Ag4k0EGcGtyGtwFWE~e@(YMxj72#*&!XYQih6KWX9r^z*1A}m z0ZNuD_CduM73}pK3MhftM9UBcKM7zXH4r&#;t|!b^HGJyg(x#gd=WrHQ5$~?1UW24 zY1gYLL&b{;BUdp&eNZQsl^fBu`m`Ey4a)e&eG=)*tX-8zSvF4taYA;>8wI!F5KW)nUPSznpQzeqD-7-SWyf?Y3x`iIYbC>xfthd}&E z;X33MedvuU?Ytc}(*G|aJ)}r5n)F?-f6wLorE8J!jperz@`neO+M^#5uDR(g_K`!U z6+YNhET$<^*$c4X!UEfo`K1s}DNn^rdI3j2#7eK>OiA=YHGXlI18)=20WOMV`;Z5Y z9t6+2+_7l?vJiV-`e00Quoa$SFf#=@2t%RE7{?9@+OGJmk)Wp`_;xlh`oOn?kRf2} zK%|3+p>W@ynh!pGu=+cB547fB`FD0-q`e}k7yFrzs|6q!=e}77s^y@32a5ZFa|dMh zfxBHW?>w}};l^K)5|YOe;&Jqn*e!6^%YDZnci)gNHOK-=H~MA=GJ~K zU8FmdF_wL}!`NKB1IQzASYlyhs*b3f=McmlT!&#YDe`{|#$*+l>@Yfi*K#%WEGd+9 z+j)JyuZ1?}zrGAqN6z)GSvo2roOn_y4Tt03J};C;27RYkYj5o1e%>j$OBa{{jSz}n zCIwzmr6$gr;eeYN(I7}d{+vfRVkRg%WTG0T(+U_ihO`hRW7du)+eeszWQ+@Oz=zy& zV$+jS@FF-(1dk)3#DnkULh=tmKVHn0id>Si$RSrfyuaUhyVrMoi<7ISH@QZ;_t@V= z8^Amqjr7cM*b)!{JsC5JZY*hupC9nYM+cVbQTRY91j(5&VU~x2q6UAjWL3zavXDJE z8o|%3{pC=XA?dVYs}BuEma zS~bJ#QgBS^JxnplqD(aCzxQR5_Qa;oBZg4)9m1K8Lz&IKjVO^kov`HSGH+rE(TkDK?=>v>F=reJvT)mgdW$e6BG(o2VnHzk0LBs z^1w1tlo7o)vjIX4E?42^zTmmDye=*~sAAakbCVj!%hElzRK$sjo- z$+C{8*wU1aW@={HCRHbonL2wdl1>k^^$5@)+rXO@L3-z9JBQLyLj#S2M?Fz==;3PB zJ10%11{JjDDlk=v0MJSfMJLxqb@~Zu!JqsIS;2!p*><=vlgVTGJXseqqv zfrQK8o{SHD`MPqyg%}0sjM&k27JVL+!E1J1-k{QcgNINyq>It^dItPN5fB%WDnUBN z>`NB9WcLw^+>95bQADOLWHHnr9Zo8o%boCWc5w766K^#Whyy~?XOdjpVV{~ETb!e~ zi)N0dWHUc{Wr_8R^aeE1LmIee{;5j1|LX05#0yo zJ@C5&-3Q=3&_IDv{(JJ>@L`xktLwqe4rE#cv~t#7KUm(_2OO8rdQ{5AWr}X_4?C~Mm||-d zP>yN|33Lp-i(V3k<hc8LhTG{YHcM;4M;QY=dZOLnepIX_=7?kAd?|Fl75H35Q!| zwv5V}2jW-CWw>&q&l_!q6U@)^Q>K+gxHXVBoOIA4eN6Cjst$sETkN`heK>mV!Y&T45LZ^x6Y(TWbO3q251KJF47>HReIprn3dohYQ z*lrVW%kK=MN*%>@ct>>>pCX3Qv-cqM$dWH!qwxo_d%-QFG`s$7>9~&PvF#lXZM&Y9 zcc8vNduGUUfscgZIZ4IP_5)9Uk?^~u+C8#yul%NcXf<&l4Eh6ERgqxi6zSuTwppI* zH(-ii_5@Cdo+tZuv%#Djwb6vqN<-a|QOx~>+DfMRsbr&fRkvsy*v!I_p}aDq|4gHm z%me6lXB!nXa4R|=t{_`*D%&rTz!?M)VFvUvUfg{0V$E4Zch!t%I`>^{2j|C7r ztiTz`n143|oWO|IOQALxmfXl6$#%>vS6v{P^aVa`VLxYh#I%7dPCyMLI-T-UdXZ-p zr6xSTC8ls`=9IJQSJE=Gl!%@B0~3{($(nxA1wFi_qx$ngMgo-IB*zYkP{Y4IjvpB1 zalarXBm0By=O{nmlgEL^Ki{U4$AF0)T9)3G2Iq4O3z=yp7=jGWty^kc1oW>8k!Tm} zp+00FExiUpKD{W;4H0dM%=Y8g{ zGQeDE=>dp?yvGM@wkuY17k4jCzR zsrKp7fRN1-C42n~cb11lhxtaoi2hfTAjb8G*z&hdMgs-}L^T<8ioUs4;SBs=NkY2H zH{^en2z|!#|9Ozid;h;k!P@3O53l_!?!Op>i0=P9Q_+S084mFUDF5CE%zK_C}Fe_XfZ}9OipzE5o}mAvaem^+b$ss@hZ~o3fKlZ zx64Z6wLx5i*l0tXm8w8pgmo^zp{cS}YcduE=*_N~2L9=#=Ac-!Y==e@vY&Iv2Zw>? z&Bc9JiloUhT$L+WZQ{3WZo8I8+R;k$lx&;8Z#TST>Z@?1W9ZAlK~)EN_}?ph`=45# ztSvf4BRE|1VV~R7dUSK#H+%s*IpCSMkv(KF2oI(#&XS90NL#=!>vc?$-L=q*hcX0T zRba7T2Zdw9$%e8(VTVXwGIH-mX0DgTS?0z~`W}%W zsML`^1ZayD%-DPsN7yW&Z`LhY7x3reFp5extS!bE$2qBxJdrLGRYXhlTNsrI0_5QunE(HFR+Ln8ov<)kE#HgwAcdk=JU- zf1oa6408fE+F2>$o^_!8`4v>oo?|3M08*P}FxudphB3>+2=C`;1(S~;+1qe$N^G~15V$S8WgDZ?L1QuVzFpMsOrNb|3 zJ-g(_9UwqqvklE`!8MlS9Lho5aFu&||P+6sGA%9FT@pgjV-VCDiiX@(L2% zVK0{eXny>F5j~7(;NL;CxFHvYJVTVl=vfgzn0XLSrhDkoKedSD4O_e#EATb$Ku|6s zw(LyOHD{TnK+1hI1fB&+Y(HolojXV%h?>2-X{(hQ>(+%`A?SDWak?Wt^|<@IJoh*h zk<6w1c+Z8yij5jAJyWuFME*^}~z43?OYA!<#|)B_!kuUHFS0p+CIMtDw&j!+lBdJAAvJ z7c_cbk1P-=it>`HmNnu7^nYVp^1uW6KV8;+6XrigI{`eX<-c6j9*zVW^DkMoW1@j_ zL;Oo^5LkV5uYajc0U89v?jLGvkHrKP{SO7o1o{Z`56iW`a)CM!|NAs6ZBWPme4ECg z3IE~j%|Y$|wf?CXlK2-!VGW83|F6a43M%&>EYbs11LEI`#W+k2$O2Fh5beMGE%8sq zB1^hI3>X%$G-*y@O9*AG9~Ly(2u2#7{zUMS1Qy^QKnx)tt3wSIQY@7-KUSj>yn(oj zcxn3_K=V8geJ-j#)1^*blnz5=;XanN=iAIH2=w~`EeNe;fy=m8dSZ^G9{MQl00`48 z6LZ}fPOYPrxud79^ACyjF3=j|k|UcFvEIY}jzj{^SW&T7*O#yaHu9i@;~=y)2Fqn- zvvEzj(#1yBH2k#pq)p{l!a7LTLb5Lc7%z4RhV;&6)Fdw*xnql;DZdurwdFK$69Hz> zWqyM8&wFK@JFa0CY61=VzfAD>MH*`vJ#%Czm48~f?xbsLKB>+w^YCC=E1E?{e|?L7 zaWMhH+>HYsk4e40nHX>{7SpALM+R$t(Qh~f!7fQIuE)zgH;Am`)YA`sm88sURx(L6 z-Cm*-5OTnD&vohyT^D0#f3!gEu7XYUGH#D=ji*_jw$?V)I=P-|Wcro@;6Hko1VVcKr@KfGD>9KSDp*s>i%RhKYKb?RZaY`Z44zeQ=ij$3^DPNZO zgZwipt~zRF1}bT;2lz%3+nW$G?aptU|+w(eRYPwN>7x^l+NP7P=gVN7TwYO^KXNTo-l7@lFC zgS)7S=JFCTzAPaKBh^9j#E;e*<_`<~x*#7O5$x9Ple2us^~^^z~>zvgE@*h zXOwF)gMv4>TkD$-k$O!+YXele9cJ@4vizuDfpD=n-VXDup^GlDEH*%#gyA1-&zKIP zh1|GzC5hZK$pj>fqN)shSp{xO>I@$c|Hpm*u+(`NC=58vKcP3hMm(4P7740}esiud zy(N|q$=Fjt3xf{&ry(*X-6RT>9avHjO=9VjSAict1^Sm$PBjz4X0HpScBLx|>%qKScEjXkFR^A*pi;|IU9n-RCXf&Y(&42LTQ zE)5j3Eu@+G_8o~fePI-_4{F3Urm6YdW_?~g6Kom7YrVBQ z9DNew1F^4!FB~Se#g}4&u&#&xt>l{IM@XIvQrg*hJMv<8TAJjD4-kTiLhM>GQc@%p z`y@Goc-wwzj5f_q$@F4k4Y)m1v@qy> z1up_l1&pB~OT?tW)C0``P(pBlP7AVCFT6bwq{DdHq=CgW-jJngfo#6q@7=Qcy7&f~ zq2-m6z!Qh8z134!POiiRwTb-?T&V&oK`4lzki`dE?gwoegrGqPG<Ogc6vjz$6tW!xRFAVGL<&X-B%t-F~OH(NXU#sjjJ7J%35r#+urdWhj@` zt11W-hjX!~mrCt04W=t_lnOIoCV`UZEl}@qH@cci7K$(AM~lbN5I9*8yA(vSL%Y)M zci|&WE+&6=D23Sy%u!)398F+i^rAiO{? z3kj$JS+4NZZwYO3&4`pH0Jc+Fy{C1;iH;LaaC#>+3wgG~5?HE0xeCi*Ie|eQZ&Qh{ zrE%)81u9^b3ag=-KoY{6Z|L)6OMu}>JYHvsp~$?j zH7cxyby%1W3u|a_`CZ$b1Xz9@mxv0eRiO@!#oLaChG1Q}`8^k`Nz(=uj^oxU35jdd z>EM55Zc@Pkn+XhPaQYqf&B0r=_tMvZ@^^_j;PV zj@HFqPwPtYq*0n3wt}Dgly(Ax&CEBR5IVOVPEcTn3Maxz1S+Fcj%4e|IraKsUX9n` z_BlkGl>0m_4t`MK+~&mAQLD>Vm(Q;#ubaQ10`+xeG>aN%1t{f7aEc12!fEJ}eB6K7 zn4Tr@s|XtvcZDe1uKLpc#yvjLpm9>pP~l8YN>(^2OPwuPxdLYqSQfXGg;Qe^k#pc& z1%9EzFJTu3L>4lF3Wkz@;kehRZ_gHp)eph;^?(w~v2Bga;J52}yt}2w)hv2MrfD*e3NB{%@sOOYLwa zT&2R*a1EluqN|)?6?ZcyPyPQuvOUej#5kP7Zi9=g_q#B1SUjx87s9gR5YE|kmYV{ z7i0s0W8(8Wk@H+)wRpB*woxtq)Ue8;(x@uurb@j&Tn)!bgE1(tF?{$7PYz?-}m($Ju@XKgGD>7m$ee3l(N;OBwEpH%oW zyp25%ZlR2@fWW+X8p6<`U<`{;3$$=RAJ3ATTY9lnD@Jo5Q5EbP=h zEeG&30w+Wzlt={1`XxxfZVKcNhB zuB#gb9UFbFCO3b2rj1*iJ23VpSt=Ps25Wk;+_^&l+87dzofdD8F-$G3`an@w8vs)7 zZuFFLltWZ9lnf&T|?0j zHT>Obtr?IKUnmvsK2kCM*tocxNb<-Sg^X3nIFcVd;bMP&mnV-zm#bmEzsA*yu7fP& z#TBTekY|5nVJSHWi&Zj#ln}_{T~;d|gtDNfk1SZ$T}w_<$z(Exz$mY?)w9iM-5d*? zjo5Q0P#il73|tb(H1roTbcv7=NngUr;-tF`2w-tUqdmke;{>>{Nqna#W4 zDVmPWZ)tzgy3**g$y{!p267h8Q^|a?07G4^=_{SvbwMX^P5-8rEUutYD{1n0np&JC zO+JsiWEp?hS7_Fy9p$^V7lxC)i5AeE?RA%M}<#$M2|7Epgh*(+7DimcY{QcFvv$6a66IEsiVlRjVjqh99j?CkcO>?l@mU#nt(sk7=lphRnkC^1<6|f z(?EZ-txqiJL5X|1r-^ivN}7oaql6%LINVTMd(M#$XS!qI7L~LR>>3XbVPPcX#{PE? z5m%OpOg{0bq>YdAsJ`ZQe}iYcd#T5>#ZJ7$rx3qN+R3(%UeW4yw_kXEE|A7aW=M3Z zUoPdPoS>2&@tXOp8v^gNCJWjX*kRVAm9 z)3G1tZI91HhJh8MXQ=Zd$eGw1Av<{j&FTrRkzR7PO3oqY@&p=oprez6o^~4frAl`3 zbWvix!@au^I<0!l8Y2_u}UuCsiG89x`&T(V26qOB1bFF5Rre~ zD!GhYj@<+2)^=4UnNdh+M^}RV84QoPQ(4WA z0%Ty7*Qn%LUS$dwCQs1$DCGL+-Ai6{tJCj5O>&6RSLi3E3T{-%P2^^+0`nsTCRz{G zbZ>DKaw~!IXh~h{S+;D$4nU);Ui5#YOUn3_wWcdZD|V3E6>^74?j*k=upst+Mns9h z9*4`vi7ane;6!q`m?1wS9`9@1g9>^#fhkcqar}0F$+C4dtDW_(Hm6Q=IuLS?O7@X^ zQN>yvTbyXJ5ED<8;t|i!`F-7u@>xU^Cz1QPzCO^+pcO{wog!~xif$4}7e;@wdeAQV z0ZSdeW`#VAKBW7$x08oJvXe*1qY62wlE=v7p_$(uKEJb-=V5c zkdJgFScHxMRhxXGl25rT@bUGPUJnPdBON62FRmQ_CUC>y3UK>NaT3XAR%%zUWwm-r?KFWVD%7shBp%s?$5yeIr1gkG7|NU3C-OT9DII zn~|POm1faF=!IRrm0nk?!@C2!(>f({`alT=$Ji{{DjmYxBv}t6HC8Hg7#hVsQ=|oL zCX$A7lFork5@-&Afn}>#SFN6v(-3$(XNz-30v&}03n|RW7v+ynpm~T+T;PH6Mpwgl zR7E;grQ>)8%&vdsTX;?U@_3aN&_e8%>Ev(&u*Qev@`-!?!x{`&Tc1*ns#v8HII7HW zRHDff=|nn7p_5fQg&U)Van%!0gVjzx^xp_w#L21jXmuMRVnAH z(#ln(5>>@4|3x{X4%Cd5*o2K5EUr7e30JGMn$}>nW64ZSHqJbPdSHz~XvJEUuA|2g z7_H@Pt)G81vbH<>n6>RM)O9L7maZpI5(Dbc3kK>m+svwosZl{Rur!%UcjwG9W~tWp;}p1_Dmj}eOf zmq$L`+O13Jpe-tG<>L)`T6^h(4c46nAAiB4(l&p(mB18DZoBWjSjz5wN21|kljc}E zVVr26O8s04rUXZ2tT|*U*&TG7O1IM!2#hsJQMu1lhO^iFfy={_qpG`0DZItr2${bDY!_b7t>1!4DYr?%{}fq{O&Ox8Q!hZ z%lM*+6t8m|-r%FzhEiXl(kuDBJ@a1g`Dp^%VWD&DW~* zI(j`{3lge={l6BZj(dh1ReBS>nSdJT8fkyxac4i(RpALe56rsjyPFT`d)&&YxQ)Os zdIkG1fx2C#chEacz0QT6cDFyic?HXOL3gS2ZteziLfoK5=whe$(0vNMmwWsB2$aU2 zzNu+;d9g&Ve}|E~>HkQs7ly>oakX(=Cf*{I52*A(+KJ{c=(8dM9|GHt014Kn{T_eX z#dBy65!e(5hF&m3^CJgT`Wx;?#B^DsDb1;lM+wx#Uc5z$Hndn%AMO#56#6)UIM7=Z zAJH{@j61xi2#gmFPrSTQysdQ(P3K0&1i3zze>|(w=jiho7DsR+BvaC-&Q?Fl@{1hq zONV=6NO!frtkPHL@3dr6nCKAb_Eh0_) z>g|4Rpg1r^7t-k5bhP`MwSn=H`Nm-gU*A>fdwg0zN`uGg3m7(+&u^z6(7%5x^dBny zkbV@GdtSD#MxXL(xMv>V2QNoXOpZHR44;^+T@|VJ3It!u(SM@SPw77~67hvq9ytrD z&%QUdDGH$wba(h~mHvmj!>kA%>E7`3=*tm!`BvFgUX$5Rea8LNe=&d-erk)8`={{< z^lJk0D zd+ZXGBt>GXBuh2|b7B{?Kv3D@^!W@?;~D)$&TX#xuzApz9{FTZn+abCSPfq(r#wNG z5_y)d*yXPGI$ND?9sFXnwS4ls-`U#c7qNIqCIsJ1QYBSN)&hrxo(6xXud30C?o0j$ zInHhg=Qst~DXNsp$<`<5Mgr##rK?f~e`u`vA#_A5n>}u)HU&ETl}uI2;;-a{y%L#d z#D@o~QnoaNK(5Zg1v@HS?k)U?k2;64@#6nS;$v`em?{nD)Mjba^6TL=j>Nu)lyijg zyya-mq*1CgI*RTd~Z`bSI^))52=Je{sND`Sxao?INS|6ZNXA=>K*QJ{+!Lu z9Cv$5i(M)r_jAH0tI`x{DuIF8A_q-EwRvd`&c4n7iRDV4q)vahENQwb9Th%47_@7; z-;!oV@5M!HB(1=5F$=`hN;5#>FlMXL94ko^)=&p=bo7D1XdrlTW-ltuMK3BXATXc> z&Bexgu650PZlAQM?-ZLYkg3XrNHGe}5>;9%l@qXQ!lQ{yJgXJ@*!T(3a*>`E*4ove zlf5GPY}Iba)`x#u&CP6=Dl}SGs?sWHwLzxOB#J$A1h7Vx)=KNN={Xug z0;e3-Zf`6BL#-;+NyidMGf)M)W~UzBu30SbHmK5Z9Nr)^Jd>^E&umg9hs3uu2#;I{ zocuqcz;LMzs^pX!QOE<>0!#MdSdO!-KY%o=l1n;Xn__>j>G?_j8`(1uwWv}nN0ez9 zF7YQks?;WJ)mBq+jasmSr>=ZQ4tKC;i)ehR=7k+eEl?UZyE_+0A3>E2D=-Rwag{ z+?_IA-H}HGhhOKmZ zTV2{xP}37PsnX5-i6Mc7LN(0}|5C>`XYg_U*sZE`8&8|*vmFC(->yn`NOx)(MDcc9 z5@orR6-&KKmG0)1CG#Ae#$BGF!#nq=(mpQBnZ|Sq^WrA{#C@uCzw`is;X$C5^GS|K z2eE&SBkEM8{Zbc!B4e312UX=={~DhI1j7R0X`#TGM{2;v~i6m z22fm~9#N%7xk3#yE0jsJ^d9dn=`mG$oHxX1ts#Ps=)xD;bKfOBsY*{tPZJmyoc|Re zu!o&P!k|5?O3(2I%MKC0jeaxU7o-;z=_P+v`mOYGTvPG2jI*xQhUv=)#{9<3_=5jl z4~8a4zf+}Gd5aozg98Y3Gx<8=Gl2*U=_%6h2^*&T9UuHas3a#wfkP z&FEVM>Uvv+nUwi0e9h<%O{7hgyea)j1zGwt0Xe^}j(7O}f^oX@1T5U`XbF(YoBMyS zs`NK*lk!cIl>(*nbVdXz{H6C)>3#mvm~LJw(<=(T@po1Fhx8$VvDg^Ac1O>Z5ZG~8 zqs>s}hTt#da6eY1PdMCB;j&{*-9J_7U(&w`lv+NHe@<@%iB;avatNGwcsoteX#Px< zKIfBwrfI1>w3+(M4>L+0m3J!^U#fr7e|ZU$V<{n$i(jkKH`2Gk7&wHB+67-PtfLrB zn(y^Gc8DP*^B5EF@L(OKiPHCUk|O;;V0siLHsBkZJq~X}$)dm!M}`HEulUEe{No2Z z14a}^RVMKzO>u;eMycU8G^9E-Z%kI1Eim2`a#K&4v4rT;BZAb#vdDxfXu5w`QuGCk zJr^6c8i(02@I!tLP+1B~)iVxedW^J4`(ZT+-Lz~e$Cj?L432GZBsN`q@u5tWWwAk8 zXB53We{O-%CIlMdiXZy`Hd|#wxN+7a#gGSrn#7+Rrn2E|1d33Yh#4DotnTM2soJ^} zmZP%VK*Be?(tz}i?xmP%)M0j{>*WHQjhNRQ>-&xvOOF+{G~N}Qrs6`@qnZm{Mqpjslc2*34%uVFRCq=7aS+q& zCUaXDQ?1O!RT7vV!@PeFEGMA9!Cf#+#jGT-BAy(o370tyY%X$j^ddvHOfeN1vU`fD z$eMUEPGRL~d%c(nTN|BoJp{EzF~>y56ts3GOhLnEU?Pw*f91-J<%>2hFRR^HIe$eN zf#kqWBw{D71h$@l4H01f3VrbWg<{*=nyTexl^d(emsFNlE-`<-o3*-Z@#?bbrTPQG z8%!H3NU3lI(%CiI#@Xo=o_a^iT8G!g&-IH858LJegyV$&HNR`%>@Xe+9gY^*3-TUJpz7mH2glfoSS zc0LZZGAi)63Kx8Ip=Yk}G!YmX@k0+Gzh!|?lJsXaZm3)%y~b%W3Q7GIR3n}0B>qbb zh6eB*-Q*T+U)&}Av>yd_Rr;r-*)=tS)pjHD!v=1eT{C|xa#NScgl=D|9FL!&JK_g- z(HE%~9geYAd~zEWZ`w?MwB?2aA-mj{ioF;3rHHNIv7hMjEz);hH(edz3ftpZ_G{fr zQcht15#URo%L@N}1;OXE7T4zR-K(W~({B=lr9?57Zg4g_+FQEWygI{T(y#U-G_9`C zAD&p$51W5Jqpl7L!VGKS`F$>*Z-KJ4Wg$mfz<&$J+2p9-5yPIkZT+#Ib(9i9HmoF2 ztZniaGsMW{3h_gXZ}a1RsWYw`xp**_SP>WXcdEjmGHr9kF~YY8cP#6Un`3;jJvY!8 zy5RMATXm&L3*QxOGKMESAmiI4mAm=pW(WUukzs#5^?mqGgJPcyzSz=u8AD(mF<1ZAY_X+o+Pt)5LL-?U%2>KDs6EhvISP&I^IUjLU!wNQsLO)^p*vFgl%bWa9Zsc zDt=uouAXPly;8B4ttftx7*)4pM`fjB2md)Ut7ZHB(M|0g*{;LVW4GE@#cByxf7BJP zFhzfEHF6}lAEq2R%|Zc}yINXAXxp;b<6YP6beH>=il2Ey&CH3|VY1v^>2U{FvBbQC zUL893P`x)+Sj%2*j9zUInFNw3!d||yslsm6wwC9>Ydj$Ud<)P>HB`9+`M;=P226FYM9ZMTfjIp)2& z_?C1v&ek>p3GKeXrgag&0~#Yi7_{k0%kW*&2rTc71@E?=rE&7So84!G&uO!~N1&`< zsu0uS`bv4XW!G8nE8IVYUwv2%Xn&zQJUL(#h~^{XDS1^%Zs+y@foM-qzcFY$|9^kt zx4Koe2agat`ghL`W4%p}Tjm_0ph)EK(zUz<-$yaHi6T!)n3gc=S4WzxsF}PqhK0^_ zhTqe!J9=$vVrQ&BwqT^c+w+W`t33i+nh;10uaM{YT;iv$Qi9vHR(Zv*zS`-BpQaj^u0 zH@*@JOW2ryxLI;ooJ2ouq=4bB-xzS>forNibl(ZfDMg0`zqS2OK>M#x-8d>L4(okJ z2u$rAYxI;w=;jC35kN3Xi^)ueZ14ZnXi8?_nFkx zhkEL-)bwIgPIu$>y-#A_$z<|D{G9hO^!jf)7Fs3*WIKG(PJOE3 z+^$aV-K&aB(I)riRRW2A-u8by9m7|HdRp3B-P+0J|C2sJv}KU!UmHvdS_xeCzv(gd zyrK`gS*FG61a|-b;-!vJZ80q|+ z%~)n7es0ckgz!jZ!|D!KMz1_#Iaf%CF|(JJl^c;XU*xKs0nV^BE9uUN#T^7PES3dz zH~*_nq=_PylW4mNgq44=%CdyMtb4I8vu|r@2;@Xt|3V<&iWFk?2?y z0>v$k*3Au$m`0HECW|=&@lC_%r(Uah z9$%cOClP4s#VnWJAseqFi#C5CZz2W&v+G;<1k}0c)2qd#V?K?SpYW;76L{$;kd;oK ze}$9xfv~w!7kGa>e!k;j+X|<@+0)=lvB|6K@@lzSk!w_Wjl4EwYcQ9aH+o5hz{IBy zAZ19?4lK6SeD;!=Kl)2r1^R@9yb6zJOM6?-E;i&vKDW4cH?=Bzes_H2-GvU$YrnHJ z6>KT8Tx*9UxlWar;h9ZdZpT_b--`xVZ`0lw@x{CqKTT@k|6 z6O9S-W&#rz24;cG${{%}Uyj@3&*AC498Y6Th(_pwoJu6*4F5G)xlxsu%H??0%+Fk^ zyjWh6Aa6k*N;Z;>{8zQ)R+LbF(kRaO4@8lTZDP`xT8>KK6}3BkitLS^II)}zixFP8 z(_vB6!{>jV5J+CRx=O4%sIHk`Q>Mt<359=i|=nj+?%nsCdh$i=D@3}Rwmob z>hYVf2Ikc3E5SSbo?P$%*!LNQ*~G;b)625)awA-3@$$Ivm)k5~Zeq>)%PWzi5`NnY z3Lk%jal`J1!XPaJ#GQ7eXdAZ2c2QD}_OxCS9*K4#;)u5x#d2Njc)j!qIy814ObADI zk_j0LAcGlf3qHl)Jr--?btwgfMO`qJ!;-}7cB};8bS&l!6C}leq_7qplEPY<8;kQ8 z74a)=cx9^qn(Cp*ACOD=vgTRwO%VpCxfOvQ4h!VK}Pp&kVZ_<1(gup6Ja3nzxE5#At+IRE>eHs z;_n4-k2B=>dW6FZnSgS-o)c7I5VWF5kn;@K#lgmPg-Aw1W#N;>f(Se6M?RNUC=}ls z5hFkmr82k?pSTG!;AU*|TVOQYhOOTr2*|>>GaywEz#D3UMnDkkXag)N89TuMdlK@W zzmSDs>+!F-1ClEaz}DKr{ooUz81jEG30n1Uj}B^xXtiJA)w}WfKD_g5x9$yafyGMC16I)CsrjMP0yq()0xW_#Ib4LAI?EewF^~PS|%94A)K`5GSeH zNhh8dZzX0XTm=c4iF=`Nxb1%?7&{QUy5Qk~@Yt66i@M;64v=Tru??TQ4_@Zb-{|-` zUneDz1NR)%$q;8d4;}zdZ8qBvZym&Dl#v-KHnaBa(Z6@X!|*)31ka$g;7xHVjKt61 zAX^{BCmzEm9!HDy1f;{0C=yRW9x8MZK0O_tgE{!-0(b!y!>h0sUPFKT{0FqpZ=!8` z8yn#-a6Y_)D)=r6(fe4--|^`Wv8I2(%kUBW2|mRq{)JcnjYt21FX1!z4!#f~(1==) z4xhsX>@>tM2cBW4qhi{y{5#nh>`WA|cDRu3L>)=MvX5bB;ZY)@{7MKT<4qnAA3PG` zBaY5SWuWMr2s?+Ji$Z^5h?IDWixd~$S&)7RA1X%y{t*V@Cn{CiAxKf+)|7b)TzLqF z;O9JY2qqGVe3nc@1i&T=^xRg`~=zX zGb|zmmJ$lf34?!1BEu@8Kn+QNb;J&JBndWf$v*+w<#!bLfY@YN zriWlA+DG|kEJRiyNr9gg*seVQNbos___cGoyM6IbJkY{#SqNJKLhzutwHo3^*oEvO z-PvD*ce$e)R@h0BAA@&R6g@;z37`k4kRAsENCBjg zd`KtbVJLqoGJI(|xBTp45hDyU7&Q!;a)~H!AiEUVqxn*_=s#oelAz{0B!$z_nkew| zfY5`OceBfM%$MqzhXybg6lIW%E|R$~h*?yTYJ!|Dw0;soo`P_vBHU?~P=^{&hZ;~b zOi(j{SU?Rydbz>f>vW_e!jNWL@HZ8mcPM{L5$t~)NG0$9Bkkm zY~V}{XYPQWm`f(GD|MLnp=NO{9HYamEGRB|fQ-h!@m-{-15#$Opm)=FoQYkSP#&*B z%+-I8N!G$(QUgQC8ptE-1oo*|_z(=ZId;yCF$VTAI(B}vhwVi*On~8n3Q16r&AB}o zm`(-B3lNeNLdj(GlNu%4bcSaXjO-#4BV>ac_XGpC=;)C3Ad`)VeFG$t<4~A484-D+ ziQgFp<_vZf>L>}sUP~j?>*}7>YidmD)qsD98d1HP5K;5tQm<dBZ&kk?>fzeu@UCW~Y zLB$q5>P;``BD1v6+rvZeK;tuPu+YXo2}8?MK_Mq2{8JGAX+~I_ptG94mmV5!FNuH9 z_(Y=zFvaM}c9A*3eyHg1k)2rESysJSGM_x7)9@%hi-9zZ#26Z#ewLiAWF>Twqj@{n zMLXDo1f`%vgc5WfT8xWOW-h>XxDZB=i$!(oQI-elZD8kx8*MO>bm~2_k^HB^1^Y&_ z>+nbtN7u6(M5~QtH=@+b;^-#4rig!c$Kkbv09#f^=w%ssz@(Q#jS338$Um1 z8^rK1%EAFu(MNixqQ|h?teCJiIuo8iCOnA%o{HKG9X&E(J-b~;aE9&|2J$owsk3lX zC}3f1co9*&jH>Vw*vW4(jCp^>a0+&lQy6HV8K|KVN5Z%USW_P1uU?9iu`DF4hQRGaPKnOAY-&a#%P@kel6gmMhnlxu(lzB zG@Br8IFB8&z-A)tR%RVN&S3j=`VQbVo`?%XL9Vp4+|vyfG%-QbFfD%`l7lpL7Z49(_x31h znK4KjRUv`mRhohXrN%C4IuY?p8ju}E80FCo(XfICi2E5B)J3-PqfWAYT>*KJxI2JV z@J3Kz5Oy_A3TcE)Q0O2?r-NY-9RfpWHVmc1Oi|acP*+i>;QJ!KAE>-ZA3Dj?Drff= z9DotE1rK9{2>XA@8Cbmm=d55(Uzlk+5-ZO^#*Kn(ItE75v8IaiOchH&60o6m!DhYE znfM5&SVn94i@P9Xpn(>ctgdV@UDihl2)m!@ffi}eUCZvm402v4xuBC=x)%*#Q75^g zviQ;K^xWrNcz18e-K)dyUKhBV!SCKsS^SLY<}Ie1UF3hZyMo>eZ{8KSDNA=9AScxp zK1_bi`F0BxX2_8^8q5=_FQPH|L)vbvv}WUMjhBX=7N+|9al zBfeV;eno%Nj0g1)3{%)c^Uy-UAs8Aw9HJ2IkjJ`-146v$!{jxs4d_MaGq}r6Hn+i) zf(-H?cK`MtAP?2B=B9l}bZiDqPwXDbT|iXSG=19Xy?))f>L zcam51zS&N$$K>^c9dNDo`U7yDUV_n5;|`EFYO#O%KMW*qWspDZCvT%A=p=vb09Qzr zM!QedkyTxwhl-OZTx$YCCM-mJ4ok19c+Sn z{9cBP^TJLzA8v-*(Dl9oZ^K{lT$7G-AQzdr45gz2hS3#JjNcRKN|-_`VJfYHIdl~) zqt$;&xH#53)o11 z3C(mDw9!jo7rhiNrMuxSdKo-GuS9>@*lW1%0$9u*V~-;jr@$Qc1bY&tc{tpl%fn!d zEuO;G<>vWO!@4}mo@USJfoeV|>##k3Mj;X*3Hc1q_;3eJP*7lC=y#yx=6;8rX(Q20 zhtf2B`&pw8y+aSEvoN5hV>%#J3#oNw4|bz>hBzdYMtUPUr0X#>xdAfiO{RZtv?j4) zz|ugS7152}sfGwp0ej9O!l(IBBYcYeS&IEx8nr)5zU(Q&rxUCZ6h%jX{aK3rSsJxJ zOTO+A!EE-tu0&N>3-`A-p%T#&{F{n@M|IIz9dOBtBB498i!`l4gUhw3lgnIyLNz2*#|sRu-ehp~CiVh*QRnI)uvH^he}u94i7N0tWaoe8ejg(L<#ax> zx|+RY)!3_|H};#@*l%HD|1oOvVtS8_y^j6X3c;G_2>yl$-bDoO^(}ouk7Y0S6wcGy zSi<>_(FlBs;ru@_oc~ws;k=#|>_x|sZe*|Muz#h)9uWe2gJo_xVswoD7mo?A|{pdh3pMK`nGMd=&TO)>Tje}+VfK$^s0 zphO`{k|0NtWA7Up+1Xt7st7ou^$oitsWCtj8LyM%4kxLh1J?678Nz(!6^Ka~Xuvcx z73uS{Ws{xC*3ZH`dQbeBjQt~!?8qM#`7;FhlZ^Zs0Qpi1Ou~Qfqoh6Tcsuu>s*{U(4S6(QhBxbV66nYDjO7nbxe=3y)@O^0-81`5keLl_2{l&B|DfJcTfWSWFwC}(53wJ?N` z;g4<>ys?=DX&zFt02x|_lq^KCUxa31ac|6mK~b;F0{cDtgKic!Vr_|d=fnU>JlZcR z&SYJ*y#pHgEG=Oeh8JYp%!Xly2y67oRZ6zaWE3*>vy*?#^RE&z*Q6v=YWjt6(C2&ym)`3TYi|l4`*t)xi$wIAh>z4$Nk6 zA}18=b!M}-#7w5a;1GP}O?RTf*AorCo@ns(M4hkv+I9FYPc&{79F_%#CxH@?Xe2=k zG8#Ymyrq9!!aq~Vw-B0p|D$zye`<{3y;Kjf)QCFSfUI#srqtBO@SeO&0;6P~A$K^S zW**v}fq!S>-%k8HtBao90o8hVn^RC^mV#gC!7X_o?f!XIA?*3OXV9Mr3s8AaF0|EK zQ8P|~L6QesybYbgR+xa_Qzb9VmQH|W(hgWFod|!&Nhd>-bgDuAV5l-8t!9J#X7(rS znQP>y0$V5AC=Wl4*i%?}@;)m0#q7^kao>ee<34F8*rc=2Sf1U-$n&y5N-1>2h+Z1Y zh>>(YqPPgf;sUTs7oyf&9Bsr%6AgYOWWM_mBYI^wz%Gvo*yRXp4+6UafnABf_VypJ ztJr_rIDn#XY)DMe+RJo zM6&*&lNHM2TcS7kBgo@Nk;ez4@mQc2JQnD?;c++UlW7>@JILe6fUs!@wBzn*(-5R* z(bPYOs`GsO(-0!DA&|Q*!@*z06b}9l9r)|WzgLleuVM7^M(@Hw7yB?;IQZ*W!ohz( zqBZ$5hJ$aTtN2Uo;UN1c5Dv1B`w|X5$Uf1@cmXRDgO{PP6W6l)c|d6EDag?R!Vi#^ zf1p+T7=xS-(dB&vqohwFMo2lYEJsKUnnvq1@uN?Lcn3yEwZLVh#RzG)okqg`sYi#k zdXMLN?hE&Kz+Q8jcy=H`%v(>32OfV8B)c9#Z-4NhaoxxXKZe(zu*XR$W3Z}yZE z*2?HPUJ}_Z>2st$0fqJp9DnOkcqW>1zxszJck|x3F0H4k}To)-!_P0);jv z!7j$&QtVw_#}bX6+6b5^vRw?-^I|;_oCud2iQwyv*14WN&Hkmkd2C}IbNzn=#d-pm zr@jX9)Nhh0_4OEi1fT!6b!_-_jIkjbfV8GzSi|RyrlMr0_citV9Q%)@LwhEwv1NlX z>d!_jL!uhnj{*)&$Zz)!?K$?DRe1J7)bNar0vj8Rc=Gy~hkk{9-knSRZ454zO+Z~P z!IqeKL|y6^pu5yBg;R?7M6!SWYw1#7jo!*Lk*l+?l}itsOXcW=OEu&E%F?C25!I!# zg-GZkl!mf?a;a~!uk{63Au{><=rXwiQB)%Ls`@fAA-JOuqb2WP-&m>0AMcI|XDz~6 zhj5OGI_K?M_FWGu@(%l6XUJ7TTmS}ZDv~XC(YN=R{h>gjt(r0;aiD*KY=!}>-f)2f zOfJwo4L%@JgEf&7M;sled$3R;F%@5^kQ_i<5y`-ja+rAi5dKNugUwcq{ae&Io$PpI z>=wShvyVdpAF@L_W3AR0(D$RRFd{zD(vvB;zOcvmQTW>G4Bz zNXGQz4yfg;1w|B*Tfl!;)CE%t{|bZ(pP`%o;vgSUHl>@jvo8aofc8vJg9;+6z)nT- zPe<}ELKAcbWUw<~4BH8X_&tf81vA+$Sc8OaU>8CYyVx|?P-CQL%~^#&_|?qz@IhCs zag!-MJBlZngM$sSDL*tWGQ*5+ji%64eO}Nxdh-?*cEYt8^ecZcy<%ixC;jFb-sJR~ zqT+|?_k2#qrGd;EpFL!jx!vgdFM|TM+h}Dh&H!I1q8JXoP?mJXDU=zH9R+w9==gaA zF)i&5(~p*ze(n*|9^l!(0=B1bn6l^~W9hB73cSF~rx6;aK?2jm{B{qJu0=@K!FYD9 zX-Rg00ja=%RG@!B3INGRAViX7xu+okiNr7@z-~dAybaZ{17-5|*oOpUTW^O1q{NbH3AL6(lhO^Gthv*Cn;vb@uRP3Mi#CdxxoIim0A3@GPiuey6 z5zePZ;rxIYIsY^we-=6a0wRA7aXgQlf9c3@J_AXRdLVxxEk+VvMG{^|68?ZByn!VA z9!YpJ4iXZ2NrK4FBy!uL=Vqj=C=?Bhk)pSeqIZy@cX_0X6#We;dM|d0v{YWdP?Rkv z_NX0$V$qHdk(`fFJ3c{jK8-^=?7h~G;Zd|>Xp9ZhyK8EFK*sIB4UtvRhb40X* zK4^@yXXt;qt-7$>mNa6zoGG*GgOhbSU`BW2?ow{VxO@2EVlLzvWm@CRi-RjCWn!WJX&B3sR!bnz&xKcZ1I1w(b<(3AG@S+I=iItwfm*Q=xt()n-T&_Dd5F>gfn+QU{#GImP$d2n^qD3LUtKJ_-O|9_xp;S8#N7;Oet_8v@qw~1W@o@K7-?ju)*_K07ej(P0hv7shRKVu(={0x zJ_YjSX)sZq4zuK=U><%imS;evJPVGIOW`>DZjfg~i#!*$$w$L!_cT!EcG!UQc{ zH$3M%y)o=4r5@cTJHx(3z zadeqJ8BnS~j#eHFyC2pb;V)Y>NB8N%N^BsID?yg)u~Di}3s#{#S3{;;gEnvtjKhEL zVtFl0laGP9axE;v?`3iwtd`fqvGN9Nl=X1Dd>nY?&2XyRV1)DopiMLcm+^KZ;dq+S z5T^-NYn|MIATJf1F48zXY-AuMs&Tr4a~j#XN>0<+>D8Ba-Ne~h)dANCc6QPcmHgZ2 zP3{QoO-i|9YI1g1A2V!QR3J`FCHcfyVG z*(M)-v79ahdl2jrZNY~D_84tpnw%+T$OHAZ7#v7aFA!~UE6-EsLrS5nO`^u0;gb^=*E#beuJU_0bXBiU@8)1RYU%kkBIn2Rcv-1RJ9x z_%$N92NCS+V=Fl2Y!d<|5R8B9lIp{=*pUeKBZ4kO@K6K-DHMTeCkQJ9E_sL*51OL$ z;4wt-I3jo=Di4PC$b(jE1Y4pbcpeeFfCyfUioo6@f;M@W6#{p31g|54HxR+^`<5w{ z{MHD((GmOw5xj#4{@O=v+-{AaJvxF95y3}@;N!k&Be{uK?Li=&5FLNPXNcf)MDRr) zTj4YeqI>Fxo)SYpRQ?YA#1D`nA3|UGZQ6?xzXWSKif`SdbuFVGNHU$RS zf(wGc&kYH+lC+=!je@1N(Xi4s7HVzdjYb*_GxVijxfqh=h^1diP-*xB zQzy?Dr>zr<0;w)&jE~9w#CN!C)z*7`2bm#DU8LR8709D&E%$%vkoHE^QriS;P4tPxxn9l<t=p%xgtr6T99YHxFScV9e zWnBw2RNEUr=Y$#0FC}v6`${<#xQIjP^XbdeDO%G(fD6=P@}NFM}K>ubh?VMTePzH{bka zDR;Z|xzl$uvlw@q9$S%I%ZokVM*DV=dwCwK9-54snzuFOB#?WHcn208_S!dGT)%F* z^9A|ax7b&w%RD9{y0X|l<|G$puXXm|i)Y_z0wN>+873^)+~k`-NUkiezeZ@O#jkQbmowJp^Vshi!^PY!+Ipzm6@5}Ce z6}nNf_ee)wOv$}s@E4QIC&M+KlGF6A;kKuDlG5j`D7?>$S#-lVJj*0WNKwiR{fMMIL8r+U>m5R4Dc8q|344hpDvpjr;?Vb_TJ{%){I-+ql@D8W(z48Y~ zzZiqNihg-o{ZleG+M0MdRE702{$2IQS9gYS&L5BaqmhK6|_{?yh}Tn zcg7oI)o7Zy;B^I+JQ?1J`mF`49SYiup0O(GKPfDnt~AJwT(jNoi}H zQCG5l<0nJi${J)3^9sFiPS{Qpme}wQeq(-9E2hy5BLqbi)aofs6TvL=!@K@?_F=>)=*0S+%KQ^yrXJ)YUXr)St9~Q+XQqX|nG&;m7tI zopNN=>Xs#`Tc(m3-;X-ix8_sZDyfWI7WKDBxbiE~DS7IJA4KCyw#`&IL>?vvDjk># z_f5}W=w9aougeCp=cRO+Rs3eDuIWM7w+n{&@@ z3@Oh_+qQWuU399%Kh}{ulxt`dNVRm!RVsgFamTQxOZ=pt?deDbztd{1MrMDSa9^>& z3l?!j!ykubzkf?$S%s~$`rVpydqc2Zr9Wp)vG{01PD;O*M{&o{&P3cqGPhkUpm89u z!#LEQTyf86K~YK^x7l>Tg}1A74arFy#;J1(Tx17w$z6+)W2pF^Q|T=h}R-*X}lmZ(C4?7Ybot;S?~~xVql`&*Cr#;-)0!sInu9C)xjWmgt}Td1#F*w z!2NW(tFnXCa@Cftw3x1}?va8&IqodKvt^(9O&6w5M`@-@BGExH#-sCesRGHblj9d# z81A1L=Fw}g_CSz|c%F{RWsS)*XmodS`<>o^dtS_4xcl#{`c%i&t+uv@sTZiS%;{3+ z%hI`*ot{xUqHo0(m`jGI>zf z&6)0XpWpdt)=?%#4)jsCcXJ!W;*zB`Dm-$}N4#hljXXh{tn%n0ZfeUzT;dYaC`TI` znF{5*&fL0t(cmu|ltD9zCy?s8S(ShJ#Emj^MBWr?n2Hn6q~JFs2CnO!pvoj)5-m5a zw$6>iKeQF;9VgPW9v@m^VsLUIxLLGs#%Wciq#C^n|0vTT;yN$YF}cq>7(YNQ)7~T+ zXBnRxu;O9ui@)D)q%mq5$gZ3}+O~U2*JXCKZ}pU{Y7W!)t|HrKl6;Co&K2Aww8Cz+#C_v1i)5+MB*9OL z*Jj((O%Cs@loV8Htb0sF7ws9kaZbtgFZHq!^blWX%IT`(BCOr^%o>N6Y-@|+3y+Go zT`^3_^!lJfd2I?iNj$-sm)$-WTIJmx>NQVZlAX#7_Yd1+U9iXT?|jEQvPDbU>W#g{ zrT_3(771^!SG8Y-I(>vvhWzc04Qj2o+rIc<@k;~waWlEM|JbsWH;Sx;lyT8dhH#-) z_cMl$G5Vt_qUYPixFg5w{8qdU(a^7o*|g*eTPN)Cv`uMkp>lqc4*kT_dyPHaWxI%r zUf=Q#zH2pDB0e*j^QK{E^5p)OpqODpCR*8i`ylwHLDY0m;zx;9w8}SAl-jSFOOBrisq(Rh zmOd7{Ck;WSY7k@v-XZ(rDmo%7_5+>45>rB3fConPd>kerCJJ*#f}#|vHvx=%ZgM$+=1)KPQ3?*^~%vKHmcVcJ>hV=LZYpVcGcfKUh3K6@O z&fXzN78;~`qWPcTm8yKu&zIRkkmVc*f{Kn5YXLgljA)7i=Qtt=TBu2_5O~@afZb5b zd59pMd!K=HRfmj%+t!=}L=Fqbd=iY=Z2>^@47{Hv2F5gOUQ5|c2YGLkfFNHXTGUIR zp$!Cec#Ci!c#W{N z5bE{+%oovc@cphEyDoa*dTAWSoK;a6A=tA@Olc)`u1lF9%9p zg`p3BkfWR#n2H5^cLrz-X`j_7i>(ik$k78kZ=i>tYYVJ|z!!!WW4 ztF1G9gT(Y)9#l|K1X08IwGm*-`p)-3eMAu=8jB#yFr7lpLWOibMo=yu(L|jjVLS>t zh%$Z_hL{oQ1_v~0j#Y-B)k040Vgje0udozqtq&{Ctvtcmo+GOYLC*XC>$9Hl%S6K> z$P#Sl4(>V^ArIzg0@Xqnnxh7F7D1NrYRtwEUzuOrR$7v@$`#0pCW zk$;}mnhQ+%JK&XQDm=Sov7^q=N??-R4dVz-4ltrH4{XZWb9RL99PtYR_1O(e>;4Dk zo_R`3c@Qjr9$0=GA@ynYKiSb7974gCfuicSgy%W_f+ye+G8VH?yHz3l z3Ks!hF%s7NKUsVvP=#I8*}LF4GD*(ee+_#4Q56j0B>Lu`HDUEaqN<(Y*z*JQ^i|$YC+@J3Ph# zmTjUC#1LY!^%3B;l3-P#nD}SH<=xUpZDb(m;$jFg5pvVnE^q@MCNijtZtPhMPKbFxm7@?e?f1C|bWlsF**yGg4hTcbgz)nZ z3h>9Jkonkx^R0jtyX0o?f|YRKq771rsv7@@sy%_br46hU@M}7U;wSbn7e*ULK 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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/148] 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 449dc14eb06fcf9cdd739717c2a87818fde9ff9c Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Wed, 20 Sep 2023 11:56:46 +0530 Subject: [PATCH 088/148] feat: Account linking (#158) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * up to speed iwth in mem db * bug fix * adds link account function * removes unneeded index * Account linking function changes (#136) * fixes bugs * more tests * Link accounts (#137) * fixes bugs * more tests * updates to function (#138) * Account linking unlink accounts (#139) * updates to function * adds unlink account function * fixes and adds test (#140) * changes for password reset flow (#141) * Account linking update email (#142) * changes for password reset flow * removes unneeded function * removes unneeded function * adds recipe user id in session (#143) * fixes query (#144) * fix: user pagination (#145) * fix: query update * fix: primary_or_recipe_user_time_joined added to all_auth_users table * fix: primary_or_recipe_user_time_joined added to all_auth_users table * fix: user pagination queries * fix: user pagination queries * fix: plugin interface fix (#146) * fix: plugin interface fix * fix: pr comments * fix: pr comments * fix: External userid (#147) * fix: plugin interface fix * fix: pr comments * fix: pr comments * fix: external userid * fix: remove UserInfo class (#148) * fix: multitenant user association with account linking (#149) * fix: tenant association query fixes * fix: multitenancy related changes * fix: pr comments * fix: pr comments * fix: pr comments * fix: remove con reuse (#150) * fix: remove con reuse * fix: pr comments * fix: pr comments * fix: pr comments * fix: pr comments * fix: index updates (#152) * fix: updated indexes * fix: pr comments * fix: pr comments * fix: fkey constraint for primary_or_recipe_user_id (#153) * fix: account linking stats (#154) * fix: account linking stats * fix: query * fix: pr comment * fix: fixing tenant association * fix: allow user disassociation from all tenant (#156) * fix: allow user disassociation from all tenant * fix: query * fix: useridmapping functions (#157) * fix: version and changelog * fix: version and changelog * fix: time joined fix --------- Co-authored-by: rishabhpoddar Co-authored-by: Mihály Lengyel --- CHANGELOG.md | 91 ++ build.gradle | 2 +- jar/postgresql-plugin-4.0.2.jar | Bin 188122 -> 0 bytes jar/postgresql-plugin-5.0.0.jar | Bin 0 -> 206459 bytes pluginInterfaceSupported.json | 2 +- .../supertokens/storage/postgresql/Start.java | 580 +++++++----- .../queries/ActiveUsersQueries.java | 65 +- .../queries/EmailPasswordQueries.java | 450 +++++++--- .../queries/EmailVerificationQueries.java | 180 +++- .../postgresql/queries/GeneralQueries.java | 830 ++++++++++++++++-- .../queries/PasswordlessQueries.java | 527 ++++++++--- .../postgresql/queries/SessionQueries.java | 88 +- .../postgresql/queries/ThirdPartyQueries.java | 433 ++++++--- .../queries/UserIdMappingQueries.java | 51 ++ .../queries/UserMetadataQueries.java | 24 +- .../postgresql/queries/UserRolesQueries.java | 44 +- .../storage/postgresql/utils/Utils.java | 11 + .../postgresql/test/AccountLinkingTests.java | 154 ++++ .../postgresql/test/ExceptionParsingTest.java | 42 +- .../TestUserPoolIdChangeBehaviour.java | 13 +- 20 files changed, 2753 insertions(+), 834 deletions(-) delete mode 100644 jar/postgresql-plugin-4.0.2.jar create mode 100644 jar/postgresql-plugin-5.0.0.jar create mode 100644 src/test/java/io/supertokens/storage/postgresql/test/AccountLinkingTests.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 95dd119e..496fcff2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,97 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [5.0.0] - 2023-09-19 + +### Changes + +- Support for Account Linking + - Adds columns `primary_or_recipe_user_id`, `is_linked_or_is_a_primary_user` and `primary_or_recipe_user_time_joined` to `all_auth_recipe_users` table + - Adds columns `primary_or_recipe_user_id` and `is_linked_or_is_a_primary_user` to `app_id_to_user_id` table + - Removes index `all_auth_recipe_users_pagination_index` and addes `all_auth_recipe_users_pagination_index1`, + `all_auth_recipe_users_pagination_index2`, `all_auth_recipe_users_pagination_index3` and + `all_auth_recipe_users_pagination_index4` indexes instead on `all_auth_recipe_users` table + - Adds `all_auth_recipe_users_recipe_id_index` on `all_auth_recipe_users` table + - Adds `all_auth_recipe_users_primary_user_id_index` on `all_auth_recipe_users` table + - Adds `email` column to `emailpassword_pswd_reset_tokens` table + - Changes `user_id` foreign key constraint on `emailpassword_pswd_reset_tokens` to `app_id_to_user_id` table + +### Migration + +1. Ensure that the core is already upgraded to the version 6.0.13 (CDI version 3.0) +2. Stop the core instance(s) +3. Run the migration script + ```sql + ALTER TABLE all_auth_recipe_users + ADD COLUMN primary_or_recipe_user_id CHAR(36) NOT NULL DEFAULT ('0'); + + ALTER TABLE all_auth_recipe_users + ADD COLUMN is_linked_or_is_a_primary_user BOOLEAN NOT NULL DEFAULT FALSE; + + ALTER TABLE all_auth_recipe_users + ADD COLUMN primary_or_recipe_user_time_joined BIGINT NOT NULL DEFAULT 0; + + UPDATE all_auth_recipe_users + SET primary_or_recipe_user_id = user_id + WHERE primary_or_recipe_user_id = '0'; + + UPDATE all_auth_recipe_users + SET primary_or_recipe_user_time_joined = time_joined + WHERE primary_or_recipe_user_time_joined = 0; + + ALTER TABLE all_auth_recipe_users + ADD CONSTRAINT all_auth_recipe_users_primary_or_recipe_user_id_fkey + FOREIGN KEY (app_id, primary_or_recipe_user_id) + REFERENCES app_id_to_user_id (app_id, user_id) ON DELETE CASCADE; + + ALTER TABLE all_auth_recipe_users + ALTER primary_or_recipe_user_id DROP DEFAULT; + + ALTER TABLE app_id_to_user_id + ADD COLUMN primary_or_recipe_user_id CHAR(36) NOT NULL DEFAULT ('0'); + + ALTER TABLE app_id_to_user_id + ADD COLUMN is_linked_or_is_a_primary_user BOOLEAN NOT NULL DEFAULT FALSE; + + UPDATE app_id_to_user_id + SET primary_or_recipe_user_id = user_id + WHERE primary_or_recipe_user_id = '0'; + + ALTER TABLE app_id_to_user_id + ADD CONSTRAINT app_id_to_user_id_primary_or_recipe_user_id_fkey + FOREIGN KEY (app_id, primary_or_recipe_user_id) + REFERENCES app_id_to_user_id (app_id, user_id) ON DELETE CASCADE; + + ALTER TABLE app_id_to_user_id + ALTER primary_or_recipe_user_id DROP DEFAULT; + + DROP INDEX all_auth_recipe_users_pagination_index; + + CREATE INDEX all_auth_recipe_users_pagination_index1 ON all_auth_recipe_users ( + app_id, tenant_id, primary_or_recipe_user_time_joined DESC, primary_or_recipe_user_id DESC); + + CREATE INDEX all_auth_recipe_users_pagination_index2 ON all_auth_recipe_users ( + app_id, tenant_id, primary_or_recipe_user_time_joined ASC, primary_or_recipe_user_id DESC); + + CREATE INDEX all_auth_recipe_users_pagination_index3 ON all_auth_recipe_users ( + recipe_id, app_id, tenant_id, primary_or_recipe_user_time_joined DESC, primary_or_recipe_user_id DESC); + + CREATE INDEX all_auth_recipe_users_pagination_index4 ON all_auth_recipe_users ( + recipe_id, app_id, tenant_id, primary_or_recipe_user_time_joined ASC, primary_or_recipe_user_id DESC); + + CREATE INDEX all_auth_recipe_users_primary_user_id_index ON all_auth_recipe_users (primary_or_recipe_user_id, app_id); + + CREATE INDEX all_auth_recipe_users_recipe_id_index ON all_auth_recipe_users (app_id, recipe_id, tenant_id); + + 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 app_id_to_user_id (app_id, user_id) ON DELETE CASCADE; + + ALTER TABLE emailpassword_pswd_reset_tokens ADD COLUMN email VARCHAR(256); + ``` +4. Run the new instance(s) of the core (version 7.0.0) + + ## [4.0.2] - Fixes null pointer issue when user belongs to no tenant. diff --git a/build.gradle b/build.gradle index c26e5e96..a3e8c53f 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'java-library' } -version = "4.0.2" +version = "5.0.0" repositories { mavenCentral() diff --git a/jar/postgresql-plugin-4.0.2.jar b/jar/postgresql-plugin-4.0.2.jar deleted file mode 100644 index b4c244925334c54f5af1823beb1d867af996737a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 188122 zcmb5V1CS;`v?kiNZQHhO+qSJ~+qP|+)9#+OZToL?x_92YFE;k>jkj?(BQmR^;yYhP zWLA82>YS5GvY=ovKtNDXK&RgK8bJRV$p16^Yta9?yttY$y|jV^BPfv4e*!iKfZXE# z17QE_X#XovURXg|LR?jiL0;lfetJqymY!h&UY4F_c6zQwg=v{}|K#ulhx*wXy}Xh!n?Z|3Ca>SpO; z?&@s!Ul>yPO{?_4fPn7)tts1oi$Tf7(bU}4Rn^Vd&78s1&e+wpPQzXmR~^mY0Y$5M z<6#vAyeSpK&VZ;&4Gl`1NZ!`Y%R0GSL|ktuuhV`#t|uq{ZQ)+>{H@R5J`W~jn(Oar zY{2iIL$}&9hef39>C_Iy9N)Q_t~2gA@2=PHzk7rMAd$q3!{8lY3BeexEZeKGmFf)6 zOB@8wJJCAr#?{~i^w{b8OkL0R%-N^u${dehb+pmwSwewRYOWf>RX!xDxw(H($-Ya$z23F>LW5y*b#^N!}BRTG07$0o`JS)440m-Fsb;$I zR$xq_(K<@do>%L0Sm*yb>dX{i7!JQ4v4C1c<>Z+3k(60_H}SR&$RM4&+k5fvKDg0hImsQD1N6^=_S36muE@`N+ zHf5T?j<~0nPrR;%jERI#{T8=b24}dON(P`0KUq_yFS_t!9X7!aiH=wf^ap{$@$mviF)$jXO5_OWjI$DNlb0c0P(V?v z;Yi}-(0?fJ8;hN3p70(B>Xl&ach7&ojfr3Hh|h;$V+ADHzQFV&R`b8leV(ss0@i~x z{i9Kl$nuFFh+~vH%SJ(!mEumFUW4RN%ae@=AdjnK!yUz0#TVj~?t8;I zjD8X>bB-H}+t~d9{?97@PoVy%@KXP`)XLP+!NS_|zsO$I@eAM*9SG=_0|-d)|At8E zKcrMuSx)r-W&RHZY}0}D(o=6UxH3!5Dz9LZ&uP7IJ!j1;l~$FJB~0z#YL%AGN!$!a zQM6P@E=tK_a$STZfucE#=}6EJ5-+8RK1o8;0Y{_LK}SPJMg!m5uF+Id?t*$X{LJ+> zYp$609r(O=wzz9s8+b9WZRmT^;pKlHYl?`p!v5(_g`;^$nJYE#aP0#mHLkY_>`uXI3}M*~3V9WZG)6CPxGilZ9n*~H8SB-Yi71%GfnA8MK zp?(5;rMMxDony9WhK-vO)1p%KE6Pl4tRkCDevI-sR+4Uf)4uF_wsXZ6@?4GrWfhxZ z25ps^yVfYS%dcv-zjOzEhcUNBKASYRD}CLR@O;~L^y3CE7BI_BP1d{Q>ZR%-1Gj}8 z=SlvsZY!TndF|@Deq%t3BTHMxrg&0xDWA<)*#cYqq4G(Rht-&;u*UsBM#pqPm!y=z z2&{l~6CF<9Hm-BNK|zmleyA$46OZp4@nD{7x`>j*%)9o&zhG86C&L37eu4K}yC}K; zd#A}tYyT{}Rb>At!d{$#;3|`RyGheXKL)u^%P*rGc*3@( zrZb*N|F&g!iaJbg;Xb@6U?a}~(+ZH*q$;*4@_l&P%7YP(93 z9|KcWUOQ_Zu&e~}iHfgm09|@8qUM}EPgtNi$gc6C!e!v-u=JLTU2ZuDk<(pq%-6E@ zBJbqTrMnzcbjo_DgzjQ^ZTMyR7gdK{r`v%cW{C440tK|{h+@06{&L-dU<9Uc$(Dyk zS!il}p(=aKTs-5WYN~atj>-UP+=^qX3V&U@stmu$B-Zl>wS!9VMl0e?`1JMHq+`By+LP~+XS?6ffK{>qbH zTY`@qM_Q*0vgnK4O^)2B-GkQZK@i7jcb)vJOJ?JobWwx|r3*N7C6J-@D2;Wen zv?$9}fUO8%4BoWNnw8sw*Wijx$8~nD^B6A*(PK$`=N?Ho@C=^t)I=+sBb`18o7+AZ zcFJVqplG4Wfjp0y_rc=F^uqMBfD#?q#f6ux{FLdRa3UdjYKRWFc70&$gk)$id)V|j5AN<(OYh32| z2k006>yVPk2gum|Fbxj(FY8lBqXCw`wLS?VPT6?Q?zEC)FZ3x2^GAt$wl_^l(g*ch z1nxfbGTeuHa%Wb#kM>7GD2<@-B-!^}P71DLCu%Mr zDgG+Wnn(ST9zYL4k=vswBV1DadTJGWFJQ@n!RpBSfNf_Vnt>%`On@Xlvb;zv>Ri1+ z-1_BG^nSrzp60D>ZCE?Ni{ltes^G3|+_S6TT5*Ycbe-43iLPl0@j}2W1iPA zDYu6+vriyIOBRWl(P}hg!HX+NOw2i0-^HHCeK0*UE|)P99!`gV87tU$uHi26 z*PYg8(#wf{kH&5CNSkSuSxFDiC20>i@WzUBzTOl3nMogfOIeptHtgl)rObu)N7?b+ zz@Hdj>#eq_**>eq2J^G8^)6T@^FaTpWp+x&;XF`vl0j~8R*YE68za60u%vZ!H8EbQ zKSf7isLs5Z=P*3{6Mutggu{4$@~!oOtFqYq5$vcM3vtn|5VZQ5j5OISAbmfHJv;FA zkaHR9A0<-^P}D(`%xwqq_>c;J#G;j`5>ObwTBLu(1AxJr@o90!g|kv^tJ(QAn$Zbt z#)ml=qFvO)8zz){h=l?|IwC%s+MLJTJ;(v-Dc{0BVo=i0qs5&Yr{{2cMLlQ+qAcvX zV!aNF1LE+65} zN&D;_#Ks7XpiawJ{K6+omQ-7{6chu;jVcMI4w@(uj$p(n#ga=?k{eZi7kI#YdZkc0 zp_eN@-zii9A2=*a0_U4Om`US)Ei8BM3Y-aXqc6|`=R^3>A-9_x-UCY*v$34*LwG@q zvAQb+S`up|K1!(8u(0Tk_&cXf zu%BFzIRIy!W%VX&4$AhB*X7p9w?!n$c*8e5v%=&NK70^&HBKe|;WZ?X%Q&3G;mF?3 z^l<6w{4*)p@YV%Qu7T_ zwV<4;FBqxyferlRj!KLzuRKx}-#PNJPWUkLZ3!)|G00BEba3eTzQz^xRjB zaDF}3+|d*s+~7IAE33#yNGhVuL_Ph`Wul6Pl{~+IZN%e+Syh= zu6^1M*D4#G$jYp-ie)WZiAQy~K+6xGjbx=!g|d#DNdx4L+8Q<0(cMN`T26^<+R>dA zVW!3%U7Ny2WG{^zA3dqxF~zDpo`S(kZHd_~ZBtyXn4lmly5)+JNQ|ehgWxI0M{L`( ztzChDJE`lcKHFT{J_`=XZt4w?_NKGlDQ*vRf>y4|h2L!Lv+w*n?#;V)opwi{t>==X zc4iRJJYH@^zhSbAOOV2wdqi8G!qy|+Cpo9qc?+7@)82MbEwDzS-UXVUay+{p&LWTz z;D=yeX-(5inxxkf>(rc}*x&hqT|rWKvfg#qIsthtSBS5JMVg3K$9Xw9%7QqzzJJVlbK1HN}<$B zp+D*G4<*^}gi{b#EK?KtI+A*;hCaRUaP$jD8G}HA#$Nu3a`@$=(xu98=gJJCeAJQg z$YqIwQ8;RoTA8TY3UlOj1oiV!Tx>*Z>{4t<7~AUkAoPWdq-ufuYe(gqAYgA!u`uzJ zGRwlkJ!b6HWR4Gg$FlZW1M3sg@!ibf_L8|SDKeL@x%a|A$wQ)VYZTmez$!?o&PV9MWu=t`D>0b{LxlEs2p8Wb9i z6iHT^8T5#-yS!1oDB#+^PVmXXK#ArgCkuO%_J{08G$_^E%qzaW9*O7PA`P50)8^jZ zGo%=I(~Uf6UtY2`vzaE#KU+$UbYb?%9J385M%T%VW8>Pn5blw=orwusCgME8_fu)8pU7d zMI6S0sD~^R8bpM7H|yDC#Wp{iV^zumhja)`H`4L}SzTiR?HcB2q`T?vm(XkE?)-X_c?cx$%D zJjABByjhURl1KDn*s^R-Re}%Is4vlq8~YK~>!ByuII{&+-49G%%ze)h7q|yrS^CVl zHLbrcWR-p5E;3FjETpyPlJh0-{o!f0@a7|t{!pv=Zb)mw1(Ceu(T-fge46a9HuZkU zi@wFK58>{y!kdUazxo%b(9hIh)yafA0lcceY+(##+D{BRq8ZA#eYi3Gdh~2y&ujBo z3QmMNz?*+ED#;a#PI8aYmNUg+HxB&i0vONJwECw85WjBM{bTWeuYX8wGSsj-C|DnaXmda~KPXrgM7T86urP>dy^m;pK)CF%RqF@fY}E2TH;BYFl3MaeRNYZSN8; z_C0O(8>|ip*9Hd)g9fU@0z@Exk|DmcF}|hozou~BaCP2<@xN{loH$wmqC9WN@V#QN zKdA`*4G;#^VSB|PdksMaMPYl@A&55nCpvHTJa3>lUS$~H)^+2GbC8P5K3|q@u7pd| z{OY`~mhONC>;0uRz?M58f(@{t^LFM2kYocSV)#cOe9Oc4Du8kx7ub3O*wsp9mKKPa z{brbb;5wLo@fuEpke4r-KaWUVJ*p4~`LK{Ok^eB^45Zj(C%pu8_+1e3dq>VDdfYJk zL|1B`Jc!G5euX7@A(!jOSpvA#MrBvTgf+!1ilwuns^1h^x2O4pzIfE%FKpy~VZo}G zN!3??!UH(t9IOm4 zs*YvW>+szpJ!PCS=aWd}=i4$+d#kCY()r@i;!LUPhs5=O_c5cNb$VnU#)F&lT+k#= z^Id^)h)cEX|D_s595x?}B-f)2<6g{X6Kyl8e8IAy3AdS)UxZ_iE*dki#9GghOrJGjW%u?SE|6mi152C z3$$j7tB87v^@UO%_1y0PIXK{h0odeTwz1{oT9Y&k59%tmc(Jaypwb`OyD@YuLij`4 zALK31;x7+ks!-k)6ZA)Zyydx3doG}F%da^h>PH-Tp;~l;OMWom9h2r39$n9uQ1vJl zU;D$9J+}B^_XU!?Bvi&5NEH9j7!Ct}6J^}D@dY&;QTy>1OmYJB`javM^1aE34-x_+ zGhfuZe>wmN{b`J+TS~iogOG2Je%yPLgh#_)*54p|qlSm1eu#u8zVBClf&n>@Z`1_G zh7ZpEQG}-hk0oC`f);4^p0O$d*0BeaanN6&rZ|G;p>LEDpn~?b$Cj}(U%5?j0S;PU zy~szHv1kKUdZPo(0)GZL9Zm(Y3XU>jd)^htfPOZ-Vt-8dCIi^>PY19EPBh~N00XfB z09$eLFP{Z0|CUOWK^>57QYe#@O0=3Ul9!S^3zIs}q7~t#w<(nUbDs4tT0c%5PG$B0 z^j4DY;PgjibST16hOS6U*1C)!cije)yIW;r7Y2_911ai5=)__PXFm(TFB>;Vd4L4x zqMlEyXLHO#!c1sN()5K$r zEuoHb>xAY(jtg&n63kw3*{J%csrLd=K?s+wIv&n3y80-TAT7cEN<^hdu$THhkt5ew zP9#|-VflirY`p7Ze06e(-}ms{=5qW!Y+UoSuWHKXETl7{d?C7DY(foWlYSPj9cBPLy+(MB)Tm?P zMTG7lgzn`TdClM6cXph2#ZpdfQ6|CM{?o1zRBHM%>yUippi zb9Wx!NcZ2LGe3FqwLgS8)1mSfgA^^sDcp^ddh29hIm?3-ZH<$B>t+~si%vKk-cM}y zhezXT-7oadbo?mmsBb^_d68aw%9Co$1z-n|m!GL0-&(&4H1;gtw9}%o-N*&U2dJfh54NUFgMB|QxoVQ91 zhaR6qe@ZxhS$ep?4PlHgAuh&Roc8(OG2fjV{`~qO1S)>>MI!Jbe>1cDEfKDojok#Z zWV6e`SQ(DqWXIZIwldLby|K}2GjpCKyut$e-s&ULa^1^eP>8Bn%0^mK({7E%iOu0* z9d6;t>ZL0}3jBtgh?J@fnnm2)p4w_HXSE^KfQ7}O)t=meEfUoxt@>q^8naDSf{=p^ z(w!w=iK)J0QYkA(jhQAjxO!e{BN^gWFFb}}XTDyYBV+OVAeASD3Wm^Dx^za5%r@K1 zjLbGm;+Xs6lCVH!$-|AGrfZeXoIZ7%;Ru;eCa_H3dEwKDswm1hrZF*d9Vyh zD?Bt0PToXRGuF0+3hK&tn#MDb~hyYu0 zUpoBP)f4#eORY@<5+|Ib&MN30OsTTqOO()MW#NsJHSeERd;}GI1Oeetr1W{368IZ9 z#k~}(@N5`YEQ8umTJeh5>+WC^{V1|SRuWS|1TL2&h_c|plYCokPZl-g5(gsZCdGR% zt`Vkge_>DARPw0tVd_xDKU}ev6Kpjoi=G;RPeH^V`+i4~l;rBc(Gd3R87WO0@ZCV& zPHG|@v6jxUMYd7f)_sX9v7N>>@6hCR5-#97t9C0yuLeEIUgJ`7N(A7x%6N>FO@m>`Q~Ud~9lY&DTr zF#*2^=>^UmjzJNiKjJemvC<~hK>1rSOKC#d1olp|Q4q@|kU*)-weYo+3h@5SPHsv@I64{Sm%-&AI6MxRh{`^@FV-Vr82 zrf0OSKgL;qWuvh`O>+GJel8h*Pz9{7PS?g=ckaXL2!TgHUv%p1J~YjX%eN-Gu!aI- z0z=V|x+IHI>4l!?_h(nb0Gkpm2GEncM5-$D0n(6wJ*qLrPI^?XTfibOAw#r{P^%zR zeT>HIB3|^|l;1xhO)l@H2R#(b8HufTc&SH`rEt`oE1hkJT((h$Hu^>*Kzh)^A}|hG zZ~ZJXyWb%{&c$$Tj&1lGlbZCgIvHE}31sDNn0DpvL8d{083~@Ak%LdcAnD$GH`<}E z@*GFI<^?N@j8kikE{*W5I##nQItTM)1Ynfw=r2FE_FjH$fo1qv9_x6|?*9uR@mSU( zKlyBHR6?3cDH;^Qn<@R0kl;ElFF-&7d$n;J4aNm!`p$TZohxy%Iot&Ef#z-x!(3?X zgS9G$>(gf@_Rp5uj8aP2c4s!>;ST27aOpX&@&nG#4Fk)Eoowy=owLGAsf74np%qU- z(RuBC0n2so4%s}Po>3qDEQU)BS?KhBA>^BHg!I%Fvajreb)3%1vZJI<;^4uBh9UI^ zzS!APnusuoiAI+)Bd?<(HrZFt4Oeo`ttp)7fbYG9o`9Bk4cFI(#EA+*I0(Hoo~3-X z$3FC|qm~uC9T;|WTr_$<`WQho8tJHqqt+H1TNz{(b6ht;a+!7vYW%wPx*9JKCbPS= z?n}0vOwU%4$s(T%+BQPOI zL7Bj@3yi54U1f5itBgLj^PQUr$UsE-H-vuJ@Vf;@#FyfrA77zIi0`#%t(swr&7c0v zpFj=%FLX%8-NLkhrOu$SUkBcePFM6&2QRvn@DW?5N&&E2^+okd9fA0!asA%cA4cg| z_9VdaR%8o;WJBZ9$n!lj5lkRfWjh|5dy14iji|^f=Rp2qln6{lhq9zDOXgMtZ zlg&?D6lq0sor6_^a`f3+4!(%f@IF_occgFJL!u7zl65|^C->6TZXyVPZtsoA3k8|6 zFqa=LKH(te6_ajY0NwT`M?#3Jmk-;vQ|VudV%PJoxb<44XxVj!-vT0dJkT8sBF9w5 zYxHE{`1SQ-0!LO%%tuyUBiHzNx}maK`-jM#UV~p#NSzXYDgeBgK_P4|EP~WOUw9uG zI03~&UICMD3oGXl8H)u5N&YAU6{@-4_8@_x^ZkLCW2esT4L1}5&je_Q5>r6^G4|54 z150b?wgtl@wT*GG)+Ee@kOhR&FaUoxRn87f%lYs@l~m+Ml<=Fkkl*tOl=qTB#;os8 zJmirX=VV_|7X2vMv()hx*dssz2Hj(b&Y|94&U1UhHz4I>I8WYKgaGix4+-f7c{Y$? zG83@pCm`CA#dDf7p4xn}FYEy4gR0OBtW}1v*KHU8k3+w(pM~?%NlJ0jxNP8KQEffF zC>9znBXSo>roAUZIuUH=b1|L?FtW)g%!B5K-Jpi%Jb7?b#7#cY(;7~Tc3E=ylEzzEanQyKwFrnyAbPJKJnPE`7+`7; zj-9qC^t=bo31??3?;^aO1>D1vd0phrxL56J+(y=XHW+ z!w1;>#xQIGJ=n^z?`GET*hwD_JNtg69R6K=vuP4VoRnotCB+ik+xSu|qPwne5QJ{m zpwmNxMy4`fNQ<@19NGdm%IK@~pqJqYz2!>iop1UeG#|BJZzOdKSc9LkP64&7iFldN zP#%R^gy~;oj&_A>F>F`GOjpb_rDrgaV%?seR#!x8FhMyZU-HXv=r73;T%IAER2vo7 zQvtNaJNpecI+1Whdlw|@@DUI1Mi;)>N`?9>Uc6!KCEXWpy)`h8 zDbBoHG|e-`(ZX1`O_K~Wy=liOH{G>YnK-z)YVY|8Q`5cVPd^icoonNBPotupgd0;p z&`2a1FSBV($-DM-HQ8kz*wkWEh<0;UAG50~6VYi-Xhul8W6XUh$+0tp|NQj$o&`wFT<&Px5VfrzAtwE@l4p?%JJt{SrY$ym#)}>utCD@j^ zENK}_t=y#BMzt$O8NBr}Vwx`QiaJK+-ZFa6ndxpZ7}^eOANO0^^YE3P+iNQfKinwY z;=RP!=WKWWiX9=`FdMBDhQqFDl;~g`4@^-#(n5a?2%iI60B|EHm?ec51kB5iRiX2# z!yY)^C{^S5N2Z|NBHYSdGes|SbzAL5S7ma! zFO`1U=%duE6K6z`x+Ws7V7rBUzskm=gXLaYmr@F`O|kii2KnRIYbHU829*lvk-kST zyz?*^7s(QoBpC1|2P8kth0bI454pL7DBKbY^BQq|V$gjO{7LDM`V0Kb>HSIZ7hwV3 zDEMg%+7hNiYLOJ*XA{#}{(vK$$E$w?vb^OJOCjOrNQC_rQG)b>pDnmxecX)wm9v%8 zjgi>CR2sGuE{$Rdsop60)WFJ;ZfFR9yG)(jVd|J!G^&(r9a4_QEyNi7B>u0v2;5)% z>2Cyf&L4yf)O9Yh5}Dt4$e9@s<;7F@BnR~*A(*$fM?t;z`o3-w*RRp6p1iRFbh$r^T@^^aq@H#sB;!1Ty?8Bru#uBoO>8;oHs1Ba>|=KXCF% z&dckXb1m>a+ns#c`}zLB{mu4;hazQ>xHuodHoT92lwY(JuDX)3cIU)uBxTr$N1JIZ zbvmxmaxTdck{hl+k2R}Xf4gS84ZBT$`_@g^nXi1}GxHo$J^r_+-G6kGg|FsVqb6OI ze${5X$|<2}4iPS4eOzXTS|jwL)#-0?_zsrYL}ooI05ViikA?HOr@mp!!F`9R!)@ug zQvjDi@zqo+IycrUQ;mQ^mw>`l{C0@x#k|pQc#7+HqNAr7Zogwzw_(c@ICx&qI6lgX z;fZk6-Vw*Jm4B^h5r_;dj4c=>D_7|OQ#=!%Pdam%eZ8Im11p|4M^&Og^?^E`>AI{5 zL(gtIGFWf(#IFJqODqcgx;_JFJ{7^dVd&oUL238~(mAK@&(+T7LJdSWbb*Vb{IkUu zgX!1ahCCLBNFH2a;o(jxX`4DN1C&Jt>&#FE)K~a9<4kX4LerYydw6(9n6Z8s!v;-P zHDgC>t{B(WFm%pZ&~VlFyyTsx=j3)mN88LHUbs*3OM2e@-Rp8=?u<_B_jK9C1_4BK zyD-?E#O`U^m*ERIh7q)cM!RP{o2pK(ai--g_;6qwY#V7xE%s}km2t7VNqk-MOYIJk z${q5l1KH;r_gj6|ReOfqu5#lB70)P7_AD3<`U2x>NX(`@X&+THUDFAjezZZ_2fyyY z#FE@O4uyyk<{eCa!invK%mfby*<~HDY<3#-2dri3pGhxQLx!uOG_bN?VO+bQfeM@t zP5XA8aGwv(X|hEQpUZY(nJW|1ZSr${j3^Am(BFY?CK;j$w#`)^)@+zBjZo}#P57ca ziBr``!(Uuq{-9`tH%nMH$rZH6#&yc%Q#%KkK8_{WE@}Er)d=Jpq%SUr$o!ank^UXl zuEVsO9+4IJf@D)oFHYViSYY8@lj<^mlTTDf;o0L4e2g}_GfA^7#?2}cF_&nP7siJP zYeepZXwYCRC)dlr%)iE{PA0~IRvNn6td^HwPEH9jdokv{&nz2foJbf#Ro)ia>}lTt z2qdb!=$v~?3lmN?s5mipxr}q#8eQlew^_Mb;Tc8P48Xb+ z-Qke+<6949r#xw)FXMXMKqsO~jomk@twWT)JVRs+J-Z~P%H>ylew0?Y4_TuaYaFEH@@X&2w|KVea%~mnyQof z_h!{>hocalx`?_6yywZ%=l>Sz@@?qaW6L) zW7B_S?EfL@?um0U!$L@7196*e&xZV7NPdA{&T~2os^G*#?1P^;V~O3@V}D7XSNPt5 zeo;9u3H{cA49_w@{d~Kj8xR}-l5?6)V$xfc=$ub{K22*yoyDc4MTv9nZ$92l^&-Pe zotqStMz`t)Xrw#GnC2MsW!$MyRngu9_$n4zoh~`B7TI*;08O zRv-OuYx{3m|7TSz&424->FjRqVr~9kj}bVHt9ZR|0RdUI009a7UolCVJD9r|+bRDC z@qcX6%`ZqlboCbi_vN;EMTR5(~N5QvQ*8DeB;Z)UKGq3<1h zk5*$$JuXo#(QR~3_sg!Yw)>jCJ}u3U>+N(7=UtyPsN3u7r@)liZJzH(*IB;nnZm;e zW8D@ok>ckvBuFVF8@}GYgcJ`=BS)exZl!LPZueKEkveLyuZ+CD{1O{jR(=yzR$(s> zSs-H3UH0!wBjL(=n)%kwa%=P*b`J2%Y#d~Xa!g4;DkuB$a^3V)ejj*p`8^>9l|R6DF;Uw4hU^X*K^TWc^drSSc$Fc87J5kDE#2%L%EQulYC>n zwQ^xQw-$k&4Gag-IeA%H*(2=~EG-Sp6cPf%E@!CsWME>gDC2qDJ~YABGLq3RIr+JB zu)}5}Q&)RbVihP@_PFwt13Pa5tjNUTZuBMCXxM~0m{rb|&`UlhEaQL4 zq>s(hG$X@DeqC7uc(C7SJiwEh)~SJP|V1VFPOr@ zbVKvk`b{D1vYb{CIK6BYFKVhLLWiH~DNR@iVHx-R=Y zNi>)PhhwO>Z+;u&mN~V31@Rh3vE)EyQGP~@w@zJ--=-^bC05v<|C9! zEC-kkCYC&lr8|`sl(v_hJZXdM(hIqY1>LhSP`YoSOoRADVoc2ma>m&#VKR$Q`O0^pzpJSl5wXm_bx4D;Opg3{$cX0F2S9Edm zMjClo3@z=0C7SA4E5id?99x2WmqY{)6tNfR%1U`OhPwpyO<`Ns49tf&cFcNJ{Av;K z1hmQ|jE#CF6gnFibQn9Pm;GI^{2^cH+KUDL>JT#W~R}quf$#%ac60LrNhgMa8E)34H1iOI~L2tfa^& z&WV=iP86aDuE%sVaF2i%cdm$tvv%4@)@yH`+_}cTrpBaKM2I~~5iwrtP8G(FpsDF# z>+fjv2*DZWPSlWt0;kbBcvg0Q8f>byaWNiTI)^|tFP+d$Z$@I_Bltn1OlDzz#mjLk z=`{?q%Q?vP__%-(K(Adm@yoJhuBFiAC!tTb?M;FR7q<MT^@xx5p^iFUOr5iRZ)bYh+boZQ+=lTq${cX%U<5yEV}Kt2fhcB zYe`;G2V}Quge10`j%4-~)QZ$w3qk_(T*>A^S7AE^7IYLnpc5g$_yLt?%9}2j(#O#)gMOi98jW@!W&I~)5476zj$Do-o_}2V%xq+xkr%g2L}!NU1Vy)rJmi>N#dYBmY;b&Xu$JgajieE4wiLtDQe;!6WbK|ywKE8vM2uTMMngk zTy6RTxQ3s5(9x;3rx*{idl1&w3A(AID`6Za=J~)c(RJ&Lk}-A8j336PY9T#q4~ZK= z=7~3({6a_;-$I2n>5j*~kx;UfB1tJT^{_KK8)OZ!wXF_LhjDI=?}|rPxc=S56Yn~` z8XMGZEgV95&V`&iRK)D{U9F_wSn56Fvgm+y41|y9=9Z@AF2O93tgy;FH>nOIvcmCF zamh8)#1Tc{(HQ?8Z%u*7dRzUNLst&#mOD8t$pz1eMPHqdYNclm9;D4PYvJIVGdkUC zq#F2VCvZ|2dG>jqmV_9 zzJ%cV7=A(Y$_z0^jeqCD1)Rc%dG2M*#iT1b?ZYc_XoAn^ z{1i%6>ui*9;nhWI8E^E`n;O`#d`~le6KBQRHVhoqVK8nx)KX8ftme43?Vg*hm%!8n zyJ2NL`sE}Oymk=Bv%BnU7NhY7f$!MVY?e}TZvMs7D?h%4pW$3F=y~o{aRip&dA6?p z?tp*S^r8f3P<gl0hx@wt(~0(5c|1ZJUtowkfiEiecMLDmcHHH!Vnolw5`wRk_<*tT z;#m}|tqrK%f3KNpVR`%;j#0pdS|g26y|jXwCi?W0*5&9pvJc=2H$cB~lJIzq)8LC1 zx!TddKv#7%G=KTt@eR!#Su1dly1aCJKyg+p8*XPXCvfi2mB7zn91|e^Fs9%zK}z;7 zEC`01gLc=s9Kwj|6gn})Go(UjQ)pM6+E|r5SKg2z427$g|Z>nQU%L@+}W7^k6TgC?l zZEH&lIxSZhYppI9(3*7Q!Hr4-?6w6FVf*a!@{sNKP-%RXH?aQ%%>BUf$1^sdzNa~% z_!k~UydlpFOFTY7%{<~eGcKDb2Gus*D4m55>o?X%Q9u=FT>$8H>G}a%RJ#Uu$35#F zyOZuA0&h_Ww)d)`CDZRCLf}Q+Jki=;Ll2tX$dLsHX9Qm&;{p4E)JNx7#-T;;MM`zb zgyJ7a2)7SYi&)IwKL>_9-?#(CEB(wa9EHur#7b*eOp|tZm$1uP=HHrq=E|I3i2g@| zj_zOai0jWTM#zKe>bN$gZLblPZ5<}+qUTtq{67SqS8u4_avJ*?C#~0s+pSKdPc0E< zup|iym(R!2sdRx9NkXBrub(3uXI+VKeMB3iY@xLbR_xe5EoRgp$wi1{OQ9&xJf$#% zB2$j+MK5<<9cJfKD9@<-$oy6;Bh9=eUFGdO>xpnx`_ECg#T5t)Nq8xbp>fRg{eSalq`LkLQEsK+X zyER}*VK4q03J{D0X9{cCin9-uc4P{qFtxiUFt}T#R#YN_RV>ZVe8d@6DxEEqHd_>m zofTQ&&`h<-kC1M6$E82EY|N3!D2I(ac8E<-WSxM9#6L}z4avveHat?SWyBKa3eoAY zDj%alM$q)-(3g;&P9zLRg4Ouo%s0aCTXT^SX8aPFIR}b}8|yRwpcG6%O2^M;Xq&W4 zPA`Zfm&EW7BG-goI%((IZmC1Dsnu0O)gJ5TjHwr$ZeCV;7E}x6AhU&o*Q6M8p;3zB zM1*=5kC4i^GZ&ocNS1Cr6J=K)EX9%KP%VjbXJ{v<<5RRKVa8-ia9-JaDZ7J6`&Tlg zN0#ZuGmq=Vqw`wPCMXk>#|)1mhbAjILD9OvMUrYgl4`g6HR)lh2M4FEYPsebXBV4c zfg3awcGFe$Hg+}^4i#J!sB7@d72Li?B<{QL5V9E~1 zzHAf#k6aPSeC=bJmEa69Zh3WxcdFHp(2GG_KK_^p?Uyz2(wzu%uH}De?{1LW zP|EI@t+_2HSWY%0sXPDC))82b8&J~6kQe}5( zZP#+)$E~$J1JfC?^j1%`AE4Ch_mc2ATod_#=ugSPsC0+=8gxxjkA!%6_KDBoq;D9YH?pJWGW3A%LC7Zb^)=v;x2IC^z~}C$T7jwUYPxcQ(b;f74o0CLG(Z)GzRSL!OU3|0UAW>)t#4 zPi5rTWH569U6?20QI8h<+|?ntMo5pg{b2LK6xQcdx0iUY%tB#*7xgJqAM_42N~~j- zWbeW~&#bSf`5&~d_f`A#*y#n`ZZF+l(cF53dfKzmcFR;w6-;GcMZR<7J4x$M0)Wm> z5rY5XKM2D=5k zpEDsJ?RQCx<4DICEz0vlXj?~P(y4?pJ8PiRsbfiVJohuflo$uSORQHvph6Z>x zNLx`_$%O{I-BV6GiUq&I<`FBok(7I8v5HnM@^{Sc8 zeP}fxIM`S+%wujtD*UY`dV~q{ww$w&m53_%H2Qwp+V!GJL%M_E?U`9ql-^7aAE2ML zWAt$rxu>$pJ68}C5`u}RkdEUGi;}_bBrIM={t0X1f+lcAm=O0Q7*AO*L*DHqtX?BbXH$J74%9lE}289r<(fnGERo_q{W@Mi^y>+q?RmX}_aijaUfiq1^O9d%OEy z>l~|*SlLa(oEgvt^pb87ZrLP!SS_>c03ZghfE_Ut{m~}4g?5h+V0Kn+ zo?h;^W1qs4ck(BfFxd184z(M$Wk%{O6M5?Hk$yhy?b*cW;QfB`r6xFd?a|X=PGVq=`0_Bvty;9SA-#6&xxMIRaI8b z2w?EY>8R_ft7_#fq0F&6&1nh01(NEb#CE+$E$Ems;|^F7DdAuN7X(>5GhO(LBj zJq`KmUqxe`SFYA$Gwr~mx;VLzhLbtFrp+{GzQw23@N#bd(b?&qJ?6l%P%L?#P*UIT zCLH#$ykk)yUB1+BXlc2!6sOgVtrhcvRfr-y5%#FY65)ohU+&@&x{)t--HAr>Tc0w- zR~$%FFnmdxJvd(===|d<%)6j6%=u}5%ELKIQ08q}OHTuL6#?N}% zo=x*AtAVmu z;BWry@_S7aj+8LU+R=2y9$Et>ry=K|#{U|&FmDS%)claNsT@eu(&_i1TM)Y^BUaY& zRLq|%e6gZvtWn!$oHd)#o)+>e*xM?CsU6>kQs}E#VYSBG-5uwm?MDdD>#D!zha-2D z=`khY$Hdt~9MAYcb6?+T1;Fu%L{sJO%+^=_@rx+dA7ScN=0W8;U~$%Dn=&n0zTA=@ z#jIUn5jAYoNMUi70I#;L)_G&g&tda?v~nWu#HY=0;jXO!XEd39iim!49&mNlwRHc= z5pi6?Lm0G&m|hEDxms_IevN^cr@HvD`K-GfU)mrBSa=F03d!xaeP0IKW2ly9Su(AA zo0+Q>EeqYP{1a9Ia*l$^b}F#8Igb%Lzc8i`5jv88e!)&amwy=t5l`@BzM>(l&O)

`t|&@An{_*h&3P!Q;}9q*wx$>{qv(AC9@H9uJQqhYVv!THK%~ zLP^V^-N}5G1h8mVDt$zKn4wSOUGa>^#-U;hhrw~}U#=ztUNhFsSrDJ=u_28@Crcg9 z2wfa4)pFv?0qjy3hZmp6>uz0g4jtGklD=Nv>~xEO^=o0-!SGB06*`;Q>T(VVrubyE zDe|n;ThUq(^*zF;4;d=Z(B#bS*l&;KatN$Qi3#XQim;Ce1u{ykoAhhdocQb5`xh7n z(ffe`z~lM)*O;$_YSFR!uy9g-^Eu!vz4$Bm!IZ)tni2-etBfNC%9Yf75h0|gHP=Nk zBXQPz21fhAOsAFwDSu!JDf8mFRL7d!d^f2q z&q-fg*ux}*ckWEJ)Ik~%!~s1JyJnIbQ?4>WQ}hCW*3Kz@QxZdQQ?_y}3ox~)-dmB- z{`%|c*^C)PUr+AcnahCARbztP-0`XN6_t-(DIC6gbK%7f_+Ok}%s>{{h# zBGek5%!uK$iTWn3TgW&V1eiR`eLqb>O-7$B=-Fh(UC;_2HMPft|Kl_AIRd<4?BEy8 z9gvti zi%}voBDj-IByu4`GCxlGp~~zix7!|1{-ey}vR#Btf6As`%kI8r(EO*_1=KM62r+~q z!k2#E<@Izf{TzhY@K)@Xw|`bNXEq4n7dJ^xk6P@L46`e`Pj#-Dg1oNG)JDx__~On= zA?;8Duux2OuCarRdkfBVuDOC>oaFp16wO6(U=U0Izk{Si)i=?75en8`)>ZI6j(x!& zsBFAWP1U%}PES?1s*VRz@wE)r>!qIuP_1GiGdD-zBcIxyMctYw|3K9r`e*Yyg9@Dc zq|8-n8+(|3+C6FFP(;VlQQ@wHb~h#-))sqEJhwW}AseMOW|uN@*I1e8r~8f|_Ieuv z7rHWkCOK%$l8?f6e2Vx;R%f<3H`06)^@=6vx;1MeXj#~JHg6)_FotKcHCXo!{iV1h zKoOE>rb1js%lwwor^d9fVl}-;QX83}B2#j$<8r+KP`)uY-<+&)cYLx&y5J3z$xIwB zv)D&5qTJ&T5VYyGdxgEd2hx0#_ZF>~*jUdWpH3N54ul0e52>0NYfBU9)2ltAO+T(K zy;V*t!X~!cd=Lq^h>z(umh`A3lUYPg;%W5v9x2P)Jk~-mr$HbOxdi=I1hp31aiPH0 zM&}yfx?MWnRo1^Fv-zi6 zxq5WDO#)s2S411RLY~eX~QvealeiaYbmf_xVx+_a(Pu-2Li6aA4Nnu6O`amsH(G$6p=e zct1bvB{%!63xO&2zRp{T!Ugoe(1;ggQ$2^I2VF$9a+6MWD=+lHr;D!lg$<%0elEaqSSXoUF~cphOuy8pzN029uO;Kh4Tu7g`m_mm3TARj)j%QCN+G!51o$=!z1 z^{F~4wNq1y`oO~N2n}QIk1#wkTjjp4zMjVA0Ub3yA(NBjg};q1ORDHuQO(_rVgh8< zpu~WPc*c=9hbn+LMf&Tc|Igpx{W`xUHL%0{0HL&MkUOj*X_Gg2(;Gb#NE+1|WdV}A z-E=zjY@3a|&T#`u`8tG~{5a}Dg3x&Cly$3xZQ{5-U*7_0qsD`H9C5+lK}TZXMmk@a zR-_HU2XWIaM2ua;XMRJYzGQPk4Ixz1LZL#M1~Nm4)plVx)%%#D2xOJwhL&a8@PYc# z(v5;tIv*Mt(n+I#e?BqrPw7Ro_^9q)HcaQ^@-lJ;jPF@0X||@ubRW#vfY_I$w!?GtcQ;5;aJsxsB#y>_rQWILe+9qJZSq z>!a$rzdAk8VS~`&yOH8Ol;S;=;yspPJ&~MAw%O#n=GNn?J5Z5_$DQFJTT%|&M}K$I zamHD@xj%3)X?LxM_OFL-{}U)B-t{Kl{YSiugEQfpMZU{!JuWJ5Al~Ig&1A$lzI?!> zetZuA!IU2J5;SkZ>PsXpHB$tyd3ilrl2VMLu6x8hlZ__A3qo1@-TaxCb8v}jreOt_ zQ3Vzqh2d0XJ$FA3bQ}Xjwhs1>22-ztsPDXm-&7dbi2Zk#C6*IEQ{Qk5Q0h9EHV&*^ zHBldI3xBH+@Kz1b$S}~BIhIrG_1Bl#9yreRUFaEqbqp1^71pB|Q(rFjFPEPOGGp%9 z##nJ7ai6)5^3w{#do0Ui*{*?gTX*`>V!r;c-gxU_-w84IBtvGj*;M)0IM5uv5m}FLytDACfdBT0wNOz{+vPRL;Z7~W#;#QbKErsW>|mTo;c{epo$JElx}`}JQeJ11@)%HrwH z$&QS8HwBsyu$HLhDT_wbx7cw1U=y)zsnk9K1)R%*!Pj5oy;ymKdgznB!=zANevBJM zIdIuQ4!4kou+x1+H%ObH)O{MaV3pHV$OCFO)SVF4J)BpNozU8SBB57w=U0%Ae$zCt z?>_rG*(@ks0|c*N=zXlJ0NZ-Fp25RCQ7$Bx!{R&W9T3kun+XV(!l6;QLDTtQTZ7*| z5!?5gR|J2Mgk7&l6zXfygZ~Y&{ph}Sd4EUv4d~%emD!}QqX&l}(@1AV;tw3KNo)qB z?D0&(tJA1)2et08V~}q5-0m@C{_GwQlivZY1Cveqr;&;7BeUa{j-Rt>;|4GuAyUOH zvnj3|TDc?6#GmTLcm0k$o>rr{9ery+K^xOnBgh>M23T_``S?p7xwa`Kx&^HuQ8xR# z6{W;XHbbfv<3ufM(AO-b98ge)F>lPbX>cN7H9OX+cS5aNVBMi#^Kiw$xEDO5afy5W zl3m&9S8SfWhq(slS}fjIuOa3dW&ShkTzm*xBkq<@iAU=&Jt(OydB>poht?G*TD4tf zq*D9zz+*MpE!z@!=WHX0wY_D3x@PoVWfi(@{t0(y|NZx^lXt9ZCv(?ob`X3U_+j7; z%xAYJVN2VCePt)I=C-7LrYES4E9;)j3;Ao=2iIrf2Zh&^H}s9eZftC4>we{tylk86 zUgAybz4;5-Yt#p&*Ge~5?nB$1dn}sn-RqsOtgF{>VpwhEmNjPEb|-;)j#zl!L~3*K47r)j#2NX5*ums=H`kc zQu&$W=AHU8=pCnckB%_^viOUyWaR`_@yJgcVTnPBRbz@K#3)Xbe=TG6PU08D9;PaG_3L{2abYp_o+^}vi6r!w}Z zU;<5Et%h5F5h^@?0~WM0CV4Yi;ckR0r|iP-XV#N&f;V7hK9eANDcV* zD}Ay~4b|@b`NW*K!=T>_Mzmld+BL8W(A9;q zdsvs1d@(VzkN=OS5B1aS9(bN4<_wy92nd~OVPJ7cCFNoJ8~~&I3=?lU(YUlT{l}f< z-X!Vvbj*=F>cua|B6bwz@c4B6LHeYJ_@pf$QviBqm&`J~TtmfW2KAFcc@uPLEkc*b z3gp`e&GoUK<8k~(NwuOA)K|)R-hdFol@NwhGaBFectLg(H%SbF=jg#xO+kr~_=F7!YoH2%cE`z6XiSXy!f@rUoDqP7Pg-f$x7;35e?A+gj@DhEuR%enEsi1WjioxTwQ zEW(&21WX+UwgDZ}h@NT8;O~5V{^ZdynBW;bhDNKjldb{SoJu@IojP&y(IaBFhS0>p zm3CZi_dl>S0i5zJ0y1-Qjpt`rx+o7YJ9yUS3#@!BXPASSMdq!xQ)&y?eH9a#NOTD} z`~|h^LMd=DTZHr%4SyS^I2s|6M^w>-x?ku7DS|O&0l-4cQfT~XL@EPb7PN?>{Rjrp z#ZmNh3Of9j7@kuclGK++$pDJt6lZ`%dydd8EI&rVYFf=Ma8s^+ROs)uI6U*;L*!>0 zG8ATyJ}PtQDs!(c$W9F8XUGb3w_^OI2>~)hpxH7|9D&GAsN`o%3Ulp6dC(k6a&8jW z(E&AytC=+3P3+sEX_5AoX@7EV3<#ke^T?<8sR?g$(%fAbr(ZR#7=40INnzNW@ywkp6CaVaxy=#$Vd(N3`wQ z#~lcH)h6N1?oU1pH;i&|ak2FC=vCk%_eF;7NMm(PvG7Lg46f2440}-Rw>%62tn3OD zdZ!V|8cA3LcF}>ev03JBR}N9+!6Yn;%Mc9H8GX1Xv)Xg1991||*jsCJsSgS)@#(P%yRh_=E=s)i3`AtMib$IxB-wnlZANM-&Cs7= zz!O1c{+^FcwuP@TWl=7tZbY-~@*O@- zDRC=H7C)vO-cnC}U$cDa`-=}itzf}whd@5#_lF=Vo??+Nmsm#?M`JTu* zcJ{7v&-yE>#na+tlph`|RDdZ(!1p4v^<3n(`nexLH5{u7@^u=N$uGtAde_Z}7wWmjST6=&wp z?jhnm(Ms6%8otkfo|Awm^_p6`NzRLMXwvs;=3DTYMI_z#ThYu;zy=6MFS3~I; zZQ`)qy5!Nkch42k$;4NI6x^T6BR+%#!J+X5TA9T21Fm!ennd*jQg)Rsg3(CuBcXa} z8N}Fe;Gs+a-w$6zOoCngf>?6P@_XDUpkIiqerV6 zhaz@0WgMx=!o2`Bfy{h#BC&b#=(g$MxO^iS;B+lwu}eC;aFcjyp?T!K0q5TH9Mr|< z$*)u7O+mZjGD)fR?`XT`7XWva&b`($s%tix?ACQU0lw{Ia<|js2&o3_t>_Z*jdisd zK)xysFkcM>sIL+M*jEh!?yHLc;Z-aE$toGZc-0i3yxIzYb@hEK@yzp@%ogMw9e>$! z2YA0Iy!m&E_?*=ws` zp-^2*)i#4ZAdL3Yi#u!w+LGUWLpr^2gP_pa9jOX=h8n5iy1E%o%>xwqTrEk zLy$MA$zAy16IWOwSBR1T3gv#K{5QA^-mZ?Vd@~2I!0n3GSVf*gRCi%s6vSoH4POwU zpAg+XA5houO1Uu3%b_Hnl=Wxo13sl@Vx3zAf5#j$QAsv5n}# z(A1?0$-M6C@Da4O;JPL%GpP4Zq!!+2Hvh(|xA#|QGh4*961EAAD~bE27n+H>qR>q- zk9_-xdi`+$nqu+0{d{lSFsZzAOxxC#Er2Qu%2|p}z*5rB&K6pGo6?OHw)|dB=(NN# zXoKj>P0DYuexSEDikmNiuQ%1Vpa-A1N0sxKt8OuaCS{N4fB0J%jd(=_(^%Ef|MJKV zUjmoBtUYe-aT$%5kAn9k|q zIL*;nGSH_^G#c}}dnFU+Vtf;gY9bLlPSQ-Q-8maZCYYI3I3f1fc}+RU*a_l$I&^B6 zu!T*?=#@_-(occEt9g+XJEgEdCYuQ6s-&Bql^6-MLHW?sz3Vqb^*VFR$S;H><;f_Z zp`BYLOEOfX4*rEm<#Y;~-+Vy~7I1CQoh=r1`8&j!&;60tqarfgp&UCpn5vpmSXJU$o}Z+g^tf;&$oa8_ta zCd!!5F8rKpS`tIDZZ&+JsO6CGL)$;t0BTfhMt*4+LmdFI zMwlw<^KElsqgsveTV8T)YS9ws4L!M#g8HUvM1w1Rd&lm9fIPYbFMEjWYV~d~o=!Y| zuz2Y~qlTl6gI$+c1`L;}TRaAtp3BX7T>1Cx?Z=JuSK*D)t;3I#xC~C+T5Dbw?GoeV zYBR%f_j8RMO>R+$5?IBn(}RC7eaH*udI|SPpfw8M~*ceQb88+l2H82gNQW7-m zT1Y*UB#BTpy9)&*pe$`;0i++KQ%4XH4}P>n(t}BL$Z4$|KP&r z2RH%RiR@v>ixgCqO}$G^hiI^1M?Y-PgvFSMxo9&s>O&ao}Kb`W!WA;)&OlXBJ7~#PQ zo}EHi2Q~KrLP)?(%YS2s>dLuNAy;s+l!UR#x=wVyi>yB*y_Jt2c^PE+D-3*XXK#Jz z8D~kOLpEL<2cHxV6Z^Yem}KUYhhT*QTh^jGe`@=fC}>E;rXTNA!O2$%&AJ#5yH)Uy z2@iCJ$BnuuG_-0Yyyfmpl$5>0$X_>j)AZ7j@8rD#rJ-|%iW*rXZKfJ!z?nmKsn0-k z?p3NIpk2*YavV?TOvHOmWH+U8OCULsMWy<-NSB1KIL*X=ho#SKHB!59n2wlTjiPjoLY1N{@RIbTi`}8ZgLb$tI)a)_-Z*@KsO_PnS4~N{v!}g|aNRN7EZoBQ^^G z+jJCU8gYY8zT^-&4yc@zq-LV1Fphf@ABWqgs9@xGB_uhbsC#g~o}Y@j<5UZ+3ifpU zI+?^8H(~9!aA(98jbM|-taY3aL%;<L&jLSs$ib$+t#x@RxBs14?ez zkhF&S6^iWf1(#+!-cOuxc?LC_SyZ} zZw^X_HpHR=WKSvI4eJrUS*YT7XR!JObyv7$t)OL8V*@;(lPDCU(4wpYA;AT|oz$0l z#w?=+3VC3G_=35K$%^k0eTiiFlzTKw+8uq8;G5SN(daUt#mM02vmUsW^-JxSf1;l( zQBPilwLO9PKB2*h!5J-$zy1LOx+E|BA61RuJLHX@rVqoBL!FSQ} z%T#aqRSktz`l0=fnMzA|&*TEv=JFcsOhKW`vE`hYr6N@4qQXDK5guohn0mzjrckF- zd^|QD6Aj;FV~TUL)c!q2EyI6H#Cd1(M}`jyii=8`fl)i>@LxoUVljsKj)#x;{a)nr z9ss+j7p5Uv#u*>gF<4}l7ivkDbSz#o$>f=DImfZa`nsn0AhjeI=ZG}QmTs6?hgR42 zB)|MceCbboc{DS5v7@R!nplM-wh~T!S@eT&5`=8-?*l#&N=rr3$0r-$iy7yOA^RXu zH{S9%cL|<=bx7EXzafrY#?yC2uhzu}L+lI;+wLE_`7`#%Ck;Pjpx*!;IdZ?{y3-x? zy6V7t7CMj{Sm+7v3mTIJ=pA@6FY0b>*$tl_TsVw26M&|>qANG+F?)vAY_ zDt(egxL*?33urm)O32*z7H&z6`2~l9_Tnb!@z;j<4w6tr1_uV{BQBEOla+cgAHCfa&S&AvLNGWW|sb>buBLt1UD@~gx(DGQ( zT83O~V{Y}r<#IBDhw*vR9a3lQ%9_!NOZy9tfR3D^n$oh%@(YiSj-2uu)5^>C3y+=- zow6FV%FE&lkM<6o@*363OJ-8=RrRjzGHW?m8~N4EsZ*@#Q`hv_TjkY{<)>ecOSjeX z)F)-t7ah{tcr2pVbt2pq+$A|z47s1f9)P0fruCPq<(Gix2F!mEZ%53RKYEtk+c~ksfB`Otv zN|JuiCjXVFB>t~UaT^m`1zT5X{U5@?e`R5-m915={)0Dw-IiWMg~@9X7Sgm7uviVC zUB*u)Yz`Oy3u4%xnni#yurO`Y!HH0D`YK%gm8IgjCpUBdx{h~zKVyQmL%4hqx}Dz6 z_^|cC<$5!P=gad2%Lk@AgwPKOD-X=gHa^!o(|b}fo!!CS+CWp0uzyr^tmY80FT|i6 z!!If&xWz{c6B2No;ZsS@b-sfTWBb1F0evIl&k7mDKZ`0Exj3Az9@75#9g#h6m$7fPY@O!3TD>_?^-*JZa^@Q8*>iWm zRvhFOqyHIqSh8KA8AKY~I#D_BxOsQN!^#1ME)$BB%zU$CH6Ka9Rl5%gOnU$$NGPWw z+>QU4LM<846ulfwB|htL0fUzK#(lpzB6U$FKFSVOEsUUAl@XJQ4#r(C;Zda=;5N?b zJnGyvrNpnMZ)6Jkd}@6Q;oEW)h8t$~{S{O0$FLmSW8gvmMH9~R>4bg75IOMb9f7h*AS;H0K05dedhx?9-f7hDQiC7% z1ds7c&q4G|8#9NZxBDc*u!^CuTcp=8sU+d8O1t(+PVq=SJ;f?jZhmUf3`Y`Jsl>?J zQM}!^;rimgV^9sbY^TIff(DrlSl8rEZ^pTd*DDicCX)OQOdEQbtK$Yn$FhJyrQ(>3 zvy9BDjs0RwN_FSZ$rRD6TRiQLrX%Mt86L@uPDqC0vy`nARTNgL7yu!fw9I&d zqXqUkjssz8L6_TBZx?@}XBZX}>st7g%`fbbw1J?{jt*GJ*Z!E+&a8d`F9v`XRPHF8S>ifYl2n8su^cNTO7iv>$J@%2HuoqUY;5YPtA1bCo*aF}`WR!#-W~9*n zYN#kX8as#^{bzdoP*VO|SbJIn+DlW_{TolS#psRH9(l7IiB%qlEZ%BZVxcQL)o?N# zQ(cC5U1m$bgGhT*!bp6%uwHU>O$DsU>3F`r<*{BMpQ{yFft9$gL zY?r*#(7|hq+wD0EafWW`r;nyEJrHIY?z9dCbtzpxSq?tDA4j@valI&Ef_R4VzKJti zyy(b&I9sZS@sOOvTXYIXW@xnF@J8oeuMA6-q4AJD)OP=&KPqd&iF0zT+@2$U21NE< ziDz4dY7~kQlej7Jfr3NnN&NjyD&e*f`U33aNdz&=^OFo<-fI$Q>( zI5~-x0iP_fi6Ta`E<;}#H-b@p!;uuTJ}(&ATPMB6fLdd^O@4r*%5<%P?#zl5Q?fJk zm26N#dHP&RB4&iq$6Z0cZCW12*_;uKLDLS&J(eL^UcsI{1<4dRkX#&~Ru?Z~D9~`c zM%7PWnVn;_fc4YmKmvKaHpa|J>@+Z3=b^w;8%M0q&WJcI1(AN_79!*|$&6$gPPo}O zH>}@*!GhsO&!rGdgH=(lJutn-zaOWtpa zR5*0(=C(ooSTe*oUtgA>(VztVu^gV1kfbOP^qam=3Q^FXwaf@Y*_GQxzZJui_-dFZ zWE(n>GhJC>F^8NatGGFiAw3E~eS%@#NC;*}%SVAOu1>%{O9xA8OMscJk6K9m*5A@H zIfB_nkQz?%tRDf4aadF0MEu>a5PRSkiL_c^#-#hwAmnm{HzL;=8dJOV7$U8XtfUgY zw9i(#aMfm%A#`!XfuI5_A!!14rXtRQ<+)&1(NmYm(N?=vb9| zc_KXK{okT=!*@mU+HdTVzoxMP7FuPa+pnOEJ5>8<_oyMv0jc}$%}Zqv=ovUVM3tKN zldmX@H}Q8Wg)-${<>NnALN~7M;O4YjXppBpUZ8YnStu>LOBQ0f-IYA$&1h}) z{x*ytbmavc#b0?6Zq_`Rio|P2Qs8iCuc7^AB(7RBl7XX*(#eS&Nxh}%VfWe~@cT;` z6{vsxN@j$~ZE|yTX6h68_RIT!eFO&t-O>iM_n;xYC^!{N>oMNqUnJd<@YH!W>ry^DkZm^8Ep{H9EN^ze~%-^qNL9zRX zLIN>DP!97{xAIsvePkq`s6LtbAxoH6vIIWiizDH zc&rJbL@1otH1zfn6TDJL{+WS_v}|A{ z3mX6;qsRe}3Qi_=eQUy?Rk%A(Pp9d6&%n?%W@`e15H1yJfN-|Z?qQKWkFgrJrn+p| zYi-xF7wB}#hEY0di9FwVeG(qrRdL%rb?`Vz`@M6WctR0kz-<(-Wpi!Vs7J;BQtYiR*F#nZ@7X} zOeh86_0Bz@IJ+8JWc1L1G_&CTUoxca*gPKr*hf@72cd)%xmeBv$kpQ$U_`#6Xh{M# z{1%q5JDkeafprGBi!Q{aLYEb$;rU+*Y)( zb9@!mFxMOTh~?ehw8!G2F`YiQnPkS95Q+&~6$1fTh*s#B(QJ#;W-_DV@%0mbrgoqb z%-B2D2kQo3v0{U#mQ@2h(yN6&Y~)>{sTB3gb2n!#RcrI?&nx^8Rsqd(gESo`^ViJ+ z`IV&`tvXm-oERLE@#&qO;DUNNuN7iB!hCJ)sk;-FkDINu?;yrQD%U~%%Q2N-+Ppuo z8`KB|OS*XJ;N-8rx33yeH>cWJzXiK>#ZAGGzlwoK3PXs(g+jjFQnBS2kix}0gsCm- z8j`0>Oe%?;8bl;$+3DtAvx#d%#Q)V->3X5b$iK+>crUY52*r-yi%E&MejtWFtQ(oz zpJE@u*7dst-b!Z;6MG!1QHimfwbYr6)I5W6aTCOHFF6jK)_Z4C z;7ezRi(CzI`|lWX{~E2qk@3te?KK@kYp|6unH?$EpQZ`amA^K4neNOA$nqUKbwv4M zC|}%a@s{0b+2Qx4Lq5BI7aZ~#yDjbnP(h_``z1C$h6~ip5MrAaEHSSM<|yX3C&_?x z0dxB=J|E^R7QbTErwNw&((T>jJ~?s}gVuv{{mbzi-Mu83zT|vb=I1rvIVDhhdIs_I zeCLv3*n@>BWdip2=bfYI!knKq&oF@j%tLG|lzwoeCe9}04LjJjiuVX2AjHj-_7KXz zEYC%hbWObY5yHhkN|37kF)O~mY+=mW;B3;OW8mU~)VwI1bBQz_D!_-E30w0%{@GgF z^OdjiRWQ-;xUR6j-p4{$<(ed8SJWDeEoQTT)!@4J0UQWy)D&-c zq~b!I0KW#m?)m900(N96R2#T>nUn@f%UVb6ghIW{adL{uad~3eDls)>fvFZ9EU&(y ziJDb#m1nR%etw;xMy*rQ%Kd3wJ0z9Gv^=`sAjWxuu*`!_XEf=34|M7zXRsV_tN*n^ zv`^;N{Hs<&4NowY-jYZ~r+1rQ=$$>j5K*W9dpoKo2e4k zA%Kkd6M|M#%}?JIW9dZRBP?=QTPCg-~O4 zHrU0EwCWxR%|#R|0&pLHP_`K#nF^8sXUqH)-1}B=_FCW|)2#ox^2saq>-WA%zs@Hd zE~y!8(}O#vGD7w4azE|@v37}nR{-@}R4TLqxcMYO!mD1+(IgX?^DDQPzl)>6OI(Nr zJ4p7Lik_~tsHAkMrDWte6ahXIr)Um&-md6up<3)O{_i{q!NOTl-Jw(vSv^{aSM~49RZXc_8%%Pj~Ai{N4|G21}FA7m1d1Ctradjo9#4e7tAf$CY|SeW*P{>iEkDrhvha==!W-GE z;=Z4}_Uqp-m|ens*1TMyFGs{D+DE&6OJDIqTj)~OYBv;Vlc*Px2;2;@iY z&t{V{SENzQ98f3gt$ZvB5<-jA^6^KusghFtiZ77ssu}2-(lHRcehn{$yxn|-1$x^B z*cNeCwMdh?=A!Ig0&?l!&fFC`!krj+2BGTy)ZZ;2OHFXhjrdEl2oI*=*U78!7rXnO z8JpZR0Pek|d=ql@bPf%v-`Cj`8yO`TZ*x4dv$T3R2s6gir7Nr5W>&YCH-niF300R(r2H*iY?aq2to(y``O}R6@LT+CQANF z8h*+zjPHVC5~OPHV7t#cLTp`PY+XWZT8M0=fItmi*wMe$cF@cnQaFvD+RWLYpURXo zaVxn*U#So8ej-1sG>-{qz^`GBRT+c#qLK%%@&~W<2e0=ec10_ihp$B=^^%jHJPWV2 z3>+Od3_IfJJA%b_d5g}Fw}ri;CU1!;mTXb|6I)lmb#KJ=sZz~F;oO{lm5tMRF1}aJJipBu&ne*Y#=_<>h z4}yaSt&omE42G6d!7C!V1@2R7r8}_B$=furcRf&kEu1WNuw-&LUS#!)iiPi%F1$kC zu%a~++D-to32&s4dhZB7UD^DqQ!lQxob}I*@JL<6&2_Kl^YmwSY##Oe9$@jDJ_Tgr zG*9<%_D-897a>m|e~OfivzNF_SNi96SU~?%P9_)p${_LWPDJ)4m^IaO&b40lZfRaK zeVbN2^hwC^Qhlg$Nj**SjMt_bt351=hKvYsbZ20;^!RL9o5X}}QtB-7i;DAuf@EuX z^*hqVq!MpKK)gYg$EMb0n@k9Tj<*ve-nv5!sx(xIKJ0diVMUEWRg5u?6hB-ySV-`U z9PcUy5^iL3jS_lPQ1pZr-m|wVDPdo&Vsm1GvsczPcq~gf$I+NRB8r(y5`;iH%r-+w zTw}1l@oC8t(G`T&gGI;jn%4(v__)bY)-~`pRlnu z&TVPX)ny-w2}{<-hh;^W-h=-mU2F+BgC(K9}znwTj({-1rxeMqCI+rNQ;RKb9NwEtH^!~ak(`3?Wm`L6Vz@_I#cQ=1?0 zLDJa$f9ROa%GR<-{C|8}kZWi|_1qB<5J2mTC(2&~S`y;G4C`0K#9{A&*<*@vab1sX zBHzb(^X7+WzhLHs0*FUuzy2OxHIvI>LW=M{nwYwrq^Fy@wDS6Tf571fdm@0$72uZ@ zpjg9XSH6eTsCQ^D;L=t*2+}nbOK&18!cgA}(MSCmfFh(Qki*Eyh&fT7*g~0sw(-m$ z!&arWufB3CIC8MvURa^rVs0-*X**4i`BzjCBZUQqabm32Pqkj6?J=f1%T9}TZX93; zVf+yMcJ5^o^3$K4+fam@r6&ct)pH1KrbA7AdS> z^Va^ZoYfB$D3Hc3O8Cqqm=&Fe6X=j}itdr=HbokNfO+7Kf<`ALsJ%D|lb&(t|6bl@ zD3_Q}=9D0UaUdFWjpaypNabk3X1c+ptFC!))74`XPm_(z&5@y?Ux*!)$<}J*>}elH(c2*g;!V{>H-=5`35g zs4E8Pw+ktZ#Y#(iX1&vF;>BKWXb=I1+b=q;BHdoE)H7pC5@>ESqjsSU5OLCK!%aQS zCJ1~P-M44|ZqYbzz5u7@iy(SD!T@5LRUoiW9Q;>|2~zQsQAxMD*vH~ZA+-g@(^9Z^ zm-(K!DCes^BcSoMa-1wUJuFk!%k|3n+|Q}*sLp+|ws6g!u4r-H zoqXOu<4DmtE9OEz|ATcNd1o@s2?7K(2L%MA|G#>#g!CQF3~cosjQ*c5@c(c8Uuo|@ z?tt-sxS9Whmszc%tBNFw{4EVZ6{IZir&@`+*$jyCW!IZBEzcSzp~A zus9L&5C{z>A?K!oXo9aG-oudL>je5#UqYyaeCwqqY4(x11Sx8BGt)E3BrkJ~k+w@f zb=CPeI#ZKV0a8Z@5{FDdBmu&iyDYUKzgrK9W5ATKbv-$-;Uv8iQGUMYDETA9R7TAH zoAJJcsrFjVzWC@diGC7}Y>HO?71vX+CDd%&WOsAd;P!!+Fr6HV(Ur1kNa{G0psDy+ z)U~(`8pxmkLo)Y|fsWFLfpfb@C1@3xdb%~ty!z(9@4%obYsAs4TC}eng5{hyk)q3R z9Yr17IRQQJx5v3F@Z6tlOD1a+6!!M~Z1e+$%+;Y_fgj|-AU?kpA?_ID_zM^ZxNT@o zB417O$6Fxi>l^u5swp~O1KUr7IyFBA2;ztrp);L>w41i&a9wFx6#rzEU|3Zz7#)W8 z!rv&{D*ET>17gutVuLhH6)yPsnK42_WW$07`&GUF z4{7iG+zSxp3&yr>+qP}nw#{#RW83D9%^Tavjdf$&o85V{TQjxqhpAU}s;mEiKHaDK zBYNjaO!m1VRE0#FE?V2aCM!E*u%3y>^hsom3cfQG0u}oyR>2uq-B4FBnq=tu}#;@?dn0`^P zPHW<_hsm1*%&~Y}EXMyWcv626KJy5Qgk7j67t0T+Rz@A+?a)V3s~(lM^}f{6e zV21D}WejT_%3W{Ot(V~uGzeuK`pqXe=J=LdjODgVE(`3JDn5OoXDn=O+RolL>r2~P z&<@(%s?lzElV`-^Y!%8T=eWz!gy$K9JzQ7JC)gVpSAb!DMdZ!J8}lo{niwl8COsaM z2%HKv2D};>{>mb`_#^bUedCvmynd?blj;bKRV(wkF)KTKKj0@?d6IU8ZyQ{>QTVKL zba21uSu3-%OJaKW7t7{3#P%o1W=Y05X4d9dtRY1%)*mUiXy4ug?t}e-9vEWa>-F_s zdOMWefeV-;(+givksrAg*m;p-4Ogc&C;u#e@XlBC2p zB<7?|3Ta4^?*jdySJxL3DDn_zxj&=OFJ@4FfRV_*xMT@Kvxrb#^lpZ>)MX0>vxYmg z56ie`>%%*2ECXJGb8jf?MmYm^Of$v;OZo-vgvQ`I`@BgH^Htd%eGxPr+)g<6R3_+_ z{~ga9=6A_``MgcT_RhSLT z`a2CZG=KmAU0F#-i405DMm(C#k3^I=W!Sv!!eMylfDuTZOv>crBA#V?-GM44D`O3@ z%k$sVRL65(hil;H*B7V|45R2xp?I)hh(`0zQ*|G&x7?wuvU7D26EZZHwgNY;ZDtiU z%oz$rR6&F$jD;M|KCN~sqpcP9DvZ@?!&^2iD?*>$s@e$fuc(edH?|(s&_rl2t9RZI zV5!~jqTzbl#?Tm9V_%&v=IoZ^gA0~p==+_bYhbMPBfl3YEeIuECHu?nz>TvCdSaqP z1Lj~m%v5xuJclbx_=a6nmzU(MzlOdw4y(eZg`%+i`{z%diC}VttUjdj#hofhI9!9c z7$oNhV4C>wV-K8YFJI&e2~}ur5yeW z!TSZS9qSlg!Ez7`I+J-hn`~;xy+pYC?n0v5>8s9Z-8`G zN^{Tn!*rj^ByVv^4aGOMzroKoqv1+Vv|YwX?CR_mI~7NcdrM0es7MXP{eyrCs$m zRmbe=^9tp=ru*V_@1BGRoecGz#V}2zW>$A`p@f?19T|BhYU_PgH*EnQ<~Ji>xYP&} zprr6kr@$_6DL@yxuufJg$%54^Qw+7-h?4DQ5BSqOG~5v- zUcF(nnaFW=5Zi;|v9O&lRjy0r?%~sD$B~4Bz+cN6O~C zTJ{sl36hr_yFev$fSF+L1S|{n3H)DyzH@?Vb1)JhAOrvZDbV--*KPmd%Dk)(ZKQV8 zB5-Vx#G9!nf(6ASWEK((FcTrV=pqKgf|Hg4iL7vzg20kyIglIN53gO>k9M!NcWx_M z*3ers(YI+~XsP$N&ljlHu+_DCa7Y>ldHKuYdj0wBJLCCu|NX4+ zc4w)t2;A>?L=H)nP!=K|>NX}c#y#6Mt+{e$yQ8Pkb;}p%3Mn!YBIH|OiY%ftf7pW1 zAX)~40N*GiN(BoxNs>z(*iPEMyhA`8;|4a=JN?zD#I*36`yxJPM`s1kZU=2mPlLJ_ ziG}l>w`epwy2`h&uW$(SDnpPvEHg^B)mgLJxY_4I5 z3all0CL4s_!-;r}!~XHwtzQePTqF;oyx$A3u)XKu0j5K0Zu^32oKd$cCAo%abzO1W z@`^k|BLlJCN4q7JT|WSp#*v<(2@u&%8zGvM-svm=C)15Ib4^;5Unocb$6fxnaYk7( z-5ci~97hk*RNq57_&{N+H1%*-+q{Zp_t3T|C%qAxaCOY>ks+c$!0g>WJSnUT2fP{jWcDTWD2dOR^$^1@RM27fMH7qLmBZhhg4{=Kb8uRp$BwoM|^cWmvs!`E3|PnO9oh{g43Iuo_gvsTal= zoV6vdTl|P0#aD=gdBeEivH|Ie(6)XhHK^o^1n&rGM9kkvs6=qGNtzys>z# zv3R|aq4h!mFd+;DLix+M({ztyCLNP>lfV6y<#l30SgP>AC>;YnjEb$chUCM(D59ZIv#T6xg2mb}imAyk?i#??o@MV3Ke#wLUgKZ)`nmKg8ABNs zuK}`7bB`8hvwzD|frJyxS5isPZO{wBlhbau#5V@QFih84+cc92`OD3u`MzU3;(9+156q|0kulbRhHw zsg}knudgR;WtO@&ldRy?;{^ePp>-(W?QU&P`7LY8t=>}v7;hKgkF@tUn9@#EH*|hm zVQK3Ts>>F05S^0AYTAWN&#B*YPZ~XMVb|I#+zuDF4z9KJ%pU|?nW7eq*W)bifKH}p zA0T~0S0C6H&qP^D-gOk+j+AG4@CmdR-iyr`2Eqn~L1zNn3&vP|BMNU2Oh`%l2y%HbgpMKt=mDRF9{Mdvp^9tWKt=fTlIp>d`1oXMH4}HJHje2Md*!d{^ohQV zj6aYX`|dElonUjIPN!=JA`Qd(Bq^a_ zVs!@xF1u+@gZc&p*X%^K+3`;nlOe|bpgaNEux+_~z78j?D$vPw zcVYjkGceQxr-Ij45Y&ibFJiHAs;tVxQ!@bmVED#ndeaXR=l4x>!P+!7I0yrFD*kt9 zVD2%DDhE6B#5ygkuW@+r5yX#Utsc(YU$@~8lq|Gyb5x9egOa4|c*77YMeqkOhGwvc z?QD3VrpS2s!5rTcMg_V4;F=c31bIdKUM_lW$*jZHM8+|^0wM?gK8y-;H)?c5)D=s= zi;$w}4mS3)qLZcOF)*C_9q5WI&?rf3&W=GtQZ>>HCL?9p9*n51vq`85*JDHYlm7zI zEjrQEt6j{GQvZyai|RRrdvZBpOe0RyC17>e_mZpZq%qwZS{b8w@W(9ibgFA+`+j-_ z`y%j|dnPKDj57v?>Zi5UHg^oAt7l#L3Lz#RH>x>Y+IGa+Vq3v=Sj4lvGkn1Y-R3Z5N9e~)v7 z3eWt}fLITAvbxEhLT%RNiUH4zCB``P1NbTi5rIlv78Icyrx%g&)c1AdC($|#UD90b z>WQ&iPXpbHZ4xw66W0=Y!8B6v=Yvc2Vcozc-Xeyxy@HF74vSUW z)&$ql1x22hs44kZ$g(CP?PA5g{E;=&8vE7Y)j0iW*s#9Zxq^vs)6s2pzBbsF$020> z@BCUPGGZ!rV4@+P4$t>yc5L0{ zD#%F&r4j%w9);HByBLIGS+P;5tb0T!!CTRBm1kjneNp{m_p+B8f)>&>ymLnR z`!%a?ID{dj9I;GSHch9etSW$iS*aLJOzp$wTAi`uZHyD+&>V34Rt+;4pj;yFto!Df zckyV#$epy5yfP)^O22-;&k(R`MHKn6g|NrIB+&BSCdnSoG*cqEcYQ03#-GNOBCnNKSVpGKe{ zy^R-(`Q9;`y7uXZAtsFDPa!c`JFwj;=A_+AT;Ak+cmF2GB91s-x*T{%G{{aV%`nM; zL99CWZwqhYAV9gXqZr!G!4Or%>++EQ`g#Y4!hCe*RLUo#(aMNKiMA-6nSc43Py0(& z!vE!4@*-pE$5Mla+c7S>cieWO=B-tr{&-$ix_D!>^Kg3JP5jeLVPAoVT^u+dPmm?R zzmXM9zt~Q$-E_dnvRfQbNE9WVgya1eiAC{VL#JBWE)yOtsW)z>q0f=reJ)Q8h#M@j67XsBe&nf;ZecYyDNct;HyKoUfXaRq#<|Wr7%rWUQ|J=XX`R#+r4O7 zLmz#+JOe~}Sg1V)$!J#=0)kz_R!<}d((=RGJE3f#UpZl z>!sKgXf)|dP{zk1f-tH<0Dgp)E6ArEGX*T*((D5r^z*UP>mWkzrY2YTk20RE)}()v z{*gPDMJTSjralI?Y+4xalRFSS07-6s=bxv?x8@PFPuZWxFw`fv=65W4fOyAUS@Qi{ zYrPcpV>rY&wwIyuRHQo9r2g*^>l9B<{O6s~2Z;A2t-6-&71|69hJe{bncC~)R$~|} zs!L3FUG)8|bZb>pdj&>S>hbBQ>5EB>z=51K#aUE07Ok#J4i*(CO<+f8+)Bd-Ypqd~OhobJ5@^UBe#bLz8cRfCc7El>zEAOdOUvEBLp<8Bd z1^X>QcZLEWY{Mm7AxY5&*3An;jNMqSboFw9Flr@3y@PnSn!Echq;B92GOUZ;zBweF zqw6&kyC}G6B?+z+J@b;PUke3F*s)4Tiy8&~xW-8`i{B=LEVsT(xlnCc_GFZ2Vwx|1 zbt@QYDi9#rJi>iuLY+D9ddr6&t_*WIk}DosS~(Ixr`l*ukuWt^G}YNOI&f*z)HFMK zCO6qzVy~)LyWwNu9o#U-ckXIxE9&)>l?~Z!N6elVs?5=Ct8ll%3TI+#ga3&rK0peX zj67&3gGXhtFE0}y4~V>z3!myxjArYg2a-3%9za*4nD3 ze%JQIKttE?NZ;TND zoNDZy2luSs8BL>xo(yQwc{5~|R?%f#7clN}O$SnE)HLEMt{F>u^5ZIaZX<%P${1EM z6sQIOrs0V@{srDxYBj59{GB%1RH$-Jq_Bw*K*s%_F2%FM>WmK-dF9(rJah2xxx$D~ zF4wR!U(LIoa$A}3qC=`^{Yqgi-pCU!*D)hX3->9zma>wAcej;(%h;@tsr0|4X=1&D z7I|KdL|A2mn~Z3dOgoQRARyr192(rRL#8hp2)*W-SxDWW>Opy(?guX!UlQZR@K(TK#V$O{zO9B2nh;S|S66!oYn-IAv zcj^KJK9F(B8#!OUX7Bojo@Zm>r7S59^&IU%zb6alN`1z|@AT)pI~%VrXH~#s0zm z6+Tn9-&D=+2N{?e+Sf?CO1w=|`7T<-BvFyVep)+~QUTN(8RdD$#xJ@+7DdZrF5|~h z1`Qn)8)b3GHZ3xM4v&$uMQhu`ejBRi4&NBgPbW0V+?vcRXwg8BQkdQ88?Ah@#G*4GYRxW)68{H_$wd*O%x_;_=Y>%dqKLIpWb z$Z9uVHgJh4ZP|payIG;51Z!gnskeTC_!_M912q;sgL+`tBKd6Zvh>7l>e3%t&D_d` zdZb>O^}<)3L{6W1-JAi^C}919FWN9q%@G>O7Ba|kHYDo;pVZk;Tqf7-;D^aXA?` z94xz)AeGp{NpVU^x$YCALram*s0<7^&A4 zdgrgf?IrRH%dHRSj>##aC|MoD9VY6covK#lJ|aNt-^sL)Wh z7Q9!}jR*Mbh;HOo?F;+(6$>&M&;k5qJy=G%!M=>=I$x>&A=sa0il|eBx88)0Hsk%#N_ zJ*pT!YEBY`JMN#|n*7{z!uuu=?5+m!AdJhJ-Pkh_PtADZs3>+DF z_|Lk9hSiB}U77uU3mkQdG59BsbNHtFUM{1RrA#BL(%!(wIchXD04s#qO`$MdGwR#+ z&!syJ#gpKkxB_4VCQNjEjOILjrs58LK-g$`-_0~ z$#c&-%0CZNS^QDY8{?=07Z}nycL&f>@RGJ)CYlma77t>D?}dbh=?Fb>CEaU#Cck& zbC)J~umr;xuciQH-ipR?9U{(7lzg2h&P(A2m5+{FpLB@!-^41+Di_o$^D*u^uS3%I zJjbktoIn7UgMyQ}o+SGroQNxh+@OVOf2KZnw3MfjN4npZe#QK6SV#pZuCD`4lq2$6 zX(35#u+3Gh!_{KobR`{USYy}(f>w`Ys5ml85Yj(2sKS`PWliUXWY04M0^555E+Q6I$SyOtX=Al||}; zOJD`)z-dDVRAE-9-{XOcM8yR51WU{pw0mmoi&m5XOQ2v}+x;bwC@=;gD?JI5yY+ zomKSYu0QzjL-~Y$9Q)+%xT!uIbi}#Jly^B3XH=Z%U!i3&qxz3pFh?3|65@tQrZeJw zL5!DKlN<-Yb*G0Qk&(4kKQ9lz>`ZtIXj*@pQ)2nX-SG&^=KWu^~4h!-<$ip5o<3NiFY|^cik>p8D=+cHWo$x5`hn&TlC6KGr9N(XQIp3CBnx zB#PvQS3Az3s7VfILzBj4BDujbk3N>PMolBvnE^NVuuN9ZK7J?=he5I2=AMJIGKF0* zH~kBY<{Yum-az6MSxz#eymN^(BTCKu{KbmuF82PS?iZ!evrl@i)Ep$jalGCamOL$8 zuMgNLb9|3A*a6hPMP>y)K$v8MRXovseThntN@4lAl<~-#HLQU>X^>XCFE$u( zX2~Qm%=bj@<_Bj>-+gk-jj*lGuEAv;5;bZ{D-fv~x7*BiQ?j^c_sLW&_N_rU>H##T`KtXg2kTkcl1z0abf-Yf;0TgPiug<#G24u1@LxF-SKO~pL zK`$gaKptIP(yQ7n$67@EQH0Ps=1!(4YCbtVz(G21D#rKE&5?Bq){gfdrj)DoE@^Ac zL8|U7zDJpi=vAC67BB_$s7Q4#`4Su?$xZxXt_5`~A_9?h6OkN=zzxM~=%i=BnD1bv z@2u}jubF$^;=PF1SA8tr*(&xEpz zFJmrEHR!jLKKk=`%&)BWv6DOu?02-7ipjSGvzZchBu%-VRYJ%K<_sL$gr|yjjq9A7 z3%dI{kkj4S&jp@(35}!`#kQ5sM97 z^u|Z7(2kX%5gt81BHSwU8%e1zT1bD}NtPwVrz$UG$fP>ph%0ndj$q;}*UjCuh;%$1 z+m5q;<-agOpF4DpoPWA#O2DPliu!cYk`9p~E~$7M5n4B0h}~JE3j({`s)=X`=~1e5 zWSIRmrQ=je!=;*pPdO5KwBEP+wRUw#Dv^gA@v*aHdOqfurc3Lp&w=WqD_6>py;M1A z;ZUZ>(Y2uxgiI#H@OmbtQ}-)`1rOeJ&RpeQPc*0_9LN=1OB|0`OBZh~{@0g9wH)QE z^JlDG3@hD+NzZSmgsmp3Hmltc;Xc_R5=O z&U!06P?li5bOnD`fIz-Rs4ada`6z$Z2%hFn7-RSW4eiBmp)Ggtmpq$(o@4FOqg6_>` z)t8|C&g6UT(N22uTy@w;2=+o;3?z7%sByATqjg+(o)5^QNaVWv8RM%bywz(SSd4O1 z@o%yk$f{*CD1V3~;Or)p^6C$%hQt6Rdor64_R+}}pJ@N6hJex2U+`!J?*ANn-DWZh zD5_J4WbG@Lr|UmCUOz-sbGQG_@mAAST@*t5;y_zWBhtExcA;Iv zAfwriA<~#*0Z%1CH~UU}YKm^>z{O!j~QHat?>MFY4kkn|FWN zw!_`k_wDx$OcZr&ELD+&tfUkR3-?I-S5lsxjkUg4Pj8PqeQ&VA;p_@ZRGm%$Qv`_^ z+Zdc(32XZ#?FfQs{p*wj>TWiuEP}d_R0ObA#b5SKf9A~nbr?%M| zMyf_~{>zG;pFBS_=hV*9n|T1P*(QhG16OQm?S*uSRHDhFoLS2vg{7>AnPLK0D@mP7 znsnR2^r$Z)EC^A7xY`Ud#4Gra(O$B}t;R;}Q7l`SBw^W)f4VL-p|cO?j=gobINT!c zFl!S7;Z1@~TaN9TRU6ZB4cAWS7Eu^}mdb&z$?AHT6Q8>usIWED57k#$)af>CuP78g zB9q0kWT8*rCZicqXyJwI3~d7uUR&d|9u#61)!BcjdkXyY(X(J)(XI!j~nsyB43 zawQW%Ks>ltqu%W5CxU6|P)+t+FjkT*?>g!)jGlD+%lL3dMyie1K5J6}`w(;S-vqlj zrF$h9D-3LrZff5a{5{}KX4i%Upr(xZm z&@O==(g_Gyuh2HdWVatMqIAu?KN%hJYgD(o8R~mQ?sfdqUkb$SQ z{0TXia7}zi5wR;YZeBhUq%D(6=u9GJDHNeGR|QR9rGE5xvPmb>Apa)jMdHnR^gJjZ zS&byQ#kKjMEX>Sd%t}1+sB!)Ql6E4l%)__HA@2T$xwMR=4G)Rr*v>9cRrXSVo<{2^ zD2UAya?Z|hY<+^K!pWkXHR(1)vDf}OGvA{f%t7AF5*J&Z&#C5aXMq2gBjXkRlCg`M_kXg<|95;$^&dW_G7>*%n7u4=O=NThRb&{=IcUB(nzvM8kP_J* zs`f++14Ea&=Q-&Y);EY1i89HLI71*H+4;)DzK~7WRIs_}`C==R^>}9L^XvQoAqY&x zc&R``m?(s22{lWz;o%o#BC`_iJG* z$LTIS8-vb_igvTv_S_3w zKj|w!iOdpk$Zt6+sX9C;Jyx;lstNTQlx1xhC)n6l9O(zBICl%M*Iv8-+c+S_W^T@A zD%|AW@iYK*e08JU@K1WtnVofL*RF=gJ0&Z+JoGY3%&M{kmzeL3h18|aAu}!nn4x8X z8e=D=Mb!A_!WxorADB6T`NJ7a=3HCV^`67>$>MuEG`cwBkC6IoYZ6STN@+2 zdii4KtYZGhuf=SPINlj&^cyI-4@hPaGV82C0uHv(#K|G_5qni^UvN}F_4l@VjzJU< zPkXnmQ2SILhqvO5yy_^FA=59tqhNtkS>Em!0b z_{|6WW?#pkeqAbDyYY8}PDuZwKnJTQa?f4~DWBnKdp3rVVtGQRc8J>(=2?dWFjGqZ zixkT?Ckg`oXJGw1=2m60)d?}79jBz;`;_+q)B{R~o6a5IJm~`j2_)xDD?kV1!*f-~q#LKruK7`$L*H$;IJ+ zUF2cz3WW2$0}bWj4`6L@EHzFd^8xVsNwFewKBVPH9&rC_|E2^s*TbLz0XfJ40SWz& zp5OnwTU8wG%w7Li`~EM2r!KUY?r@91Xg1pWU+0uRQTUW;_~dE!>(RMfurnbCV^NVN zlm?9rOX?TrVo`L|NpQ~Vvewqryv<5!jgIoVgGpejX+Zh~DJ^crVZFV*wbze5t6@*{ z&E2f7qs5mtzb}LccSe_|ax-1O?(@wp82%mg#R-A}*}qJRj0Nj~9#SVDfYRIEgdkgB6(|LhC?Q=C^zpOdD1{n-3om0fMht!x z_~zRnzKHJm3$we8*%Cgyhx5N*;D10Gj|i~y%@eeCF4SMccZKe?cEVY}$Zxc8_VDUu z<+X}X1!qD~sT*NrAg!3uLl*+(o;#j>mbV8!&mwV+<636W3u`#>9$whOT+uQno6#4e z3HmhQZ_On#A`&pUD&o*ZxYe+Xf}*L=F1AnB%m6en;GRTGyVxuu?ApvzwN$~wFj^Uv&LQidkN4;XKOh=fKr~dWTqeBJ@K@f%>R>^eQbTJ4nmTzFySYsx#$v% z`NUGn#i$b6<;SVUTWOT6MyViGIs=_vm`+!m7)Qe&5D#abyh`PSlg*dhk%uU!m4RrB z{pJppE{A)s?K27nJ|zI^i?wbF;fp2`0g~M>OcAzpqtNE5Ryx-~L3|FfJ?~GDzlf<3 zI&>wxEbs$k`Eo&rntHfYx=KBU@edKOSHw{GpEw{^0edRp7wI8Xlal>1Jyjnn`CSXe zF@yoih0X=qN>U@k=rCG=gXfB;0V!!+qlgdfB~2%OY-JS5i2&u<(1B(zKx2xA<+G`P z?324F=AMwDQ4qtt73;M6(C7>E!XeBYst1Dq>(xNQmvB%#b9G;J=s*sl3XMljt<;mY z0dhu+sUo7X|F^tV?oUFb?-Ekt2Cd0fA^npru}3S&aX)3d90nH}1!G_ih2lWg$;T?k zDF>)u(uDTmql928TU$L6gi$?6D3jQccl}cw>_PTxCIu7of+g$1alZqo7m`JOsiywF zMsM?7b76rHr#-b0KO7J*f(woTzwk;t1ZFv5fG=f)H0=?g|CKt^60A{5p#bLIo3$iA zJQgi>4=nbaI2~J+*6LK}veOtCdrga0H9>kqNhjLgnxwjOIj6mxn+vgNT7Ly}8Vjwh z_9~2i3TBP^?3AMsJ+3W#Bei^uttD;RZw*#Av_W^L9$*KhU5TE1^M94WMoTkFGle8C zD1PfeoU~4`e&B^7joJNitsS-xBq`dtJjBAIPa9Z&Hjq~o$Tu@G>S>%PS(~KtB636_ z+OugOS0@!JpPr27`2XDj2E&{%4>1P(o%(dcmFz~g9csQAJY6d~2Gu|%f-+~y475Io+tHz#-G z7Hy6E5iIOy=2Gw#U7IZ=ZPr<=t>%@jhZDqh4O@U?8dN3oNkPbRD`6XO;j#%qznGsO zpxvuLRv=E%4!gYCrs9c(VdiSzaH=o#J zlR&694$AOdu+g=M|IpCum5I51T_|qLI@VxDvq7aaZvWx6a(oR{g;tc<#J=%|(s@TV z(&Wx2GIqX{*p%6g3N9);OImUN7n5k2e!k5?;k}sKTlmsY?w3(BJ_bMOfg=&r}pj{(K@=ViT&+KIT%n2)-s6?iI5X%6>R>oq zq#AY9)IYTt+%&J)IT3eG6kq?ZUwGcfJ4vLE9^xd81}j|Y=i+GjHUw$z9*ogc-B_^v12h6CG=&vDbAw&l*znp=j6avy zn(Fb*s$HetnTB(T&hAHT`g%h#Bo^d?e$}Eq`=z9k@G`2z&6H%($+$!WrsQ@{nl^e6 z;n^?B-8wTTb;Yn_kXX}8d7ZlN6 zMo*W+3XJ%<1pInOIjy_vlzc+ypzoa0($OIGQXGa<+{$Ly`Rxj7=UXGkPEUWP4)vw_ z&_c!S6w{x=2SiU#d+LDtNG`q|pPaHBLX;oGB7Q6mp%=%r`v(s_-5_9t~g)J-#|Fb5ZL?KV}#B$AzU0gPm@Q&-oQmWp4?zObdw zxh1L)?2-6@{%&TAnT47uREpM~Op0f(FVwdRi8IqpFU@T1!@5xbQo@3Xv{LYn`v9h+WJz(j4(`V3?^we}TRP1W1jtAIZJ3H83 zubXb4t&tx7owBpnzi6JhB8YHx)m&=kag8rtoIf}@a}o=#V)gVy#!3&J^K5?hXm)g2 zN|Y~OtRuE_@vwKf_(vlG`Zo(0@x5jkV;_K~(}?_gH)R1yx~cNUj8*c-pi)i=cSvtD zfx$l;X@z2r6G@#C;w1)*AWSJ~E~wW!&xl+!RB#$hqc*K3^5Drj>MLF6TwoyTzmr`goF4GK(4%(bK1?bZ;guk^f>+jXjHUz3?YMWtL1 z$FhT3wC~BCrM!O`@%9W20Q1HUJl!-HSx}fD&N-ahdl)TmFHU+7SLIf^Y_Qe{HVxM~ z;RzM_JC}cJLu$B=K84Xj*<&S>Gh?HR!`Z!i$gi3OcnF4r_1C+!5F<1Z{WNwI5in#% zP$&$;P>2eiaKqUS9qRLYChP9D3LIHaj6^L`prr^gzL*v{lEkH2rjYqaIQEJ%_@$Om z&%M@lLkNeOj5727tD7xy<7*L_iuF;L-H|xViYe$LgK>{6dNT`gIt#!$KL%r!cSoG);SHyQAB6XM4@lb&kyW~PtT1#5Oy z@mrI6%Mb@8C+6~Jik0`IkY%}xX|zvlfQ8Ko^uRlM*}evNHI{fAX-%HmP2=UrW}iPQ zx_;85I>osHIOcD$cHBbMu`D1a?ll!i>E3kdUd4g1oJ&ZpYhiF&EP6yzy9~LJMg_G^ zSa8XVv3gpreDq*Pk~Ip_q!mK*b3%0|0@lx4sqj+xATg@ZjEV!8v|A*vq{k6-I+NA_ z*u96!aG-I&@Nqwl0ccFHH^)%2EPrv}Ul_J*RTagl<|stjyMeg-zqH}UzE1M;cFOW{0}=!7&*MP}N+l5d zBJ#jLO2K<&6~(iGc0$unJR!Q_b}(ZY%e_0~q&}gU@Ip<}pLl(U zSX4!EE3qTUf#RjuWBU8QOmKfG!ImmPnj3}EScMEX{Q4Y5)pT4@>!-Z|eKTh@3|oZ~ zqMr84)!_y^g@?9IZ7A0g!iKbM7MZR~RrWxK9bny^HuGSI>WXe-U&CSRvV$)xqw12H zZWQZMAZ@k>xZJMm!Uuacl!Mb3<3P_pSJ~|+y9M}f!eEQ-FOMs6O@#_=`ND>)4jS>m@*}(`F zYvbH|v!V9Wgx2MT(A5|6&8~1E-~uZM9Z!68ym|%^UjvU#@{72-c6^{HY+!xnA;RvN z)LHgW$etM2dhQj2@dr^t)<_(fGUv}&xkkN<4)Y$#~yhfG{l1Lpi>Lr{GB{~hM6d- zE}GMjZdOOJPZiT8pKDx^j%x+8aNc}%-VFf#rs>z0Yq~q}{C()gE2Qaf8fuT{!DPhm z?MX4GJ+`bbtI(MjTD>YJR+!C1Fj{t2XOV$to zZAaP9bzyQcX*BHAtn{GRfqsaFz8PRfUDs)CCV8=diXqUbsi zT8te;nI-;%TH6dMZU|)*Yv{JpjBA_t^tvqe*Amwk?3eDYSLB*GuS3rt=K#5=4x#E; za97x*<&m9*>+1@Kk4)3QMbgO`XXY)H$8<+GEAK^rmt6aP+o)1k{XnQA--R^VNR>$% zTQskIjqQd~AX<@26T7`kRqcjktY0RdzrKT-zu9RPY{;K;g&3uys+*IvEZ#q>D{ls!|WCwnMyy7mZ#O0J11R1y9 z!2hk#CU>-?Zg%q6@8`)KX-p011)FN^RwV2_iboyL^dv=ROZTzL1cogc5)W00V8rc{ zz1=6B_351TNoal}EbL^9eFHwJAJZ`!v&-fJDPn&EWt&b`3qq_4Kyx02(Dwy@kKji( zAVxP3S{%$&RqT2*oZcjX*UMw+TP6O(~p&QQB zf{APr;#IYET>JiI4K67TZrI2ONVWr>deXnJ z5^rR>{;CSLs+7O9rB5=-5*5SFha&a&+; z+qP}9%eK*_TQh&$n0e#3(}Yp-nCFkml~<$Ou#F~D7QkUUp#fPk~_Bd4ZzJ4qsAQMQZ79G$gyf1 z5EUGdYaEbj926*OWSI9K?ez3h<(Xnnqm;mE5~ymF7<+Pte(3V(X+7csb4gtuLyf{D zsWHe?RaCBdeXW^p1w?hea(aD1-F@DkKKSdIZwXYNe|U2b+NkHySE^zYYoUFueaumA zMobd_Y6xYzSYtN1ZsUB)Q5fC|9vl`K+!`3%iYL8bBd=Xa%jqhfT;<81$lAUs9gO7HufnhVF>Uy^)}XO|bzP~Ef>a~6>U>ZAkzAaYhHL?zmxaFE%-4mnT%A`J zoB;O)H%1JP{qzTh^IVd@p!q!-522y~xhFm+2FrK(2p_%&ahOnJ@h(nLvA)t#x`Yz+ z=p?z{18H7>;%{y$OAn>FK8bjBA_+%CJVRu>PDs>-=okE4kIch$Jk2!*4FNKV0SZeG z(cjpxPm}zw5M3|f%I}GI>CE{l{?0_EZx3JZP2@lNt$C|8Z^cy+j4#KQJ+c_4x(<5W zV7%ZbeG&X?oRKS_$eV4!HWb?mG58_L}K}!VmB}0S3~v&#;0(IneB@ z{0Z)i1nu$S@n#eBspud#@q}lsflK5eMu-|#BRX2<>apJU!%MxECJq5wSdLH^1t+X@ z&eO95J7Q^YZ@08ZqbfYaJrw50t1zBO0N_hdQ&q#v$^p;P&u} zX)-B`-P&QDoz2QOWy1(rwpln2M5Q;-@u`)H=sS*k`FSbSR8^sH`AmYU)}Dz>Z4)U0 zN)&Ar4>B}qM_py9Vp1o_c>wbmGGvS+E5az@ULNf3*bhrHscMLxM67BN%8rNtX2OQ| zE}E84#eyFD7c|eap~pb1wUilU_GJrG(ma@%*+>`eRpiz#&>kl#k&sU~PKr?@5E|W! znMO)B9Lfpr!0H}Nvlm(Z{P|iH$~je_n+x$8WydQY6mgTc1lp)O5RAlftwNBkAxn<- zyuDp==CZP_SI%W z%M3aT?l^9hiEZl?0lu{GC6$Gj0~z-OE-rlEx^ZN?X!!vFY#%tUw7L&l4yW6m4He-5 zZ1IE>ZhjH%s^%0f)F;?l&Vd%UTuT9gPByiMz|HYobI-y=HTtE%nnO%i_mZMg`1>E0 zXQrD8MRTl1Q>U(lo)+Px;c1ftwRWvFs##|fG`X}6AeA&7g9VMZpKJBKm~;3Y0Tr?T z#ML|g)Fi~W;Q`klP4TA~MZF#L?zd84Q!RX7`Bf3)afq+|p(bQYIaAOExnHx84(>8+ zn||PZp46=AeH;_~SlWIvY7)^M_PAz=;4O+^P8MjPiB2AtkrH~tP~(G6ViEw)&|?31 zfSlc*ra$`!m&J$9G{LI-C2k-dB+nLYq*EznWfe&4&!#5u-EkYbYZ%|F zamaoNOL~@V?v^-@&mJ31lScIVF7gdcEnYjKEijZIsu@3xqKS69W(5RDpFP{z&#|3m z5sn9^a1238_H}(8yIPdKM(N254MH#@yAf59KdPxwRuU<&Ws<96t?FKSvNXklSuNT+ zyp>bVf2<-kAypug=kH4=kiT* zyJL#C5ie^J$MdPMql8ZzsGl<9C^NjD*}w8_4HoM{+4Vzlx_K-Cy8#i`gvvDv@gUM= zM+%_;j>!x@w-5P+VLl-AEq|PJj-ahMD?c+x+`;IMzdMcs}VIl zYBD06uFs?T+J3*wh+EgM1P<8o55e+=9u>^Tt7at-qAYvBx>S_CvVrCWLQvk7UdZg{ zT~j{QxxQ-b7fIS4B<3evSrjYy>?;7c!f1djj9fr!(U@Nczj$tvQ~ ziZRb0>(KohJQ`n(!0&}f$kduzM`R2)FdMtBk^Ou-8h~Pc`6dUhvnHYVPD+K}3Fs;x z?!BL>Xx*t!dzzRpulIQu0`kc}ppa~R3j^+2ipOSx)=V?;9kI^^1U?5r0FbM>TLS$K z>mS2NWrW=feY&>&CtE!N*5zBMI9GiKtJ-z4u9rXA;9WV;ps&Wth(V{Kb>8CVJp4?* zL)8rr`=$#d&T!RKVWMzC8@t?~pg1Y`4#mX{ap=}HlDe#^k0En_;!Z4X_v>G{sG1G< zfFtE22GpM<*Rt*}H1AW;0UA35H#3X*--Vh?(%@Drf~?Y=>!2Us1^SGbVc~{a4#5G5 zUWb6rs@SLN-{ZGW5g=cO6S&>i3&N{EYUUDV1qBd#@@DCyRgEJM!(tVa|CXK%;etpO{r?Y}k&(rO=s5+S&@6$Y9G3_9fls{a4)aUx?pp z&oi=Fv1vh>X5~WLGfT@q!s%iHe+DZxBWtN6cgp#*8yS2B^|QuTlIE3rgjbjGA482l z>FsWCD7%`kz5KYu=|67|L)7EB5Hdks%MetGsGi;mD*4&DKf)g z6hWfdoSud`O=Izj)thw<6RBXls33@;?EZ8-J>8(+Oj@s0ZB&|*R!<+X=rF9+x^i?4 zkzvBIOU*dj_Q3|m@x<#}AvrrLN8%h!y}sRBnI68h~}^rhHx*>VBVnskrpRZ{G74bmo|n{$Z)NMSRmO{p%W z9D&5SQmIKm*dniZv4R16>>4WMy-&TZll0NukdfI}q?X$5Faian^={Cu0oLezuxA!~ zEeD1ytL~s6V!Z+Jufkmm!Ws}d*#nu1rP!r%$m1|POSi4z4YlxRKO^yA-pSB#XlN^86>+kos|3(nJ$-n>-z*4#++}Wqu~gcWP#1 zZWCqiofu$II^mlGe-yujCRcIdmyzb6c~`3lK9w1Fw2AsCl{-o%?3&+>Fd0Qb#yu(GM``&=HLlTTuk6W+FTsBx~k^daT7- zs7CT!G#|_@L0NCNUoE-8<|u)g z)}m$!rIyg49`K^#D@x_Z4BpY-syHjCbl@4~LHV%$WD^>Q^ zj<48yQ*@Wr02p7pOFMd3HR~QGcemxH<(dkcD&6+gOYMbO&6T;uD{VxqReV@ix|=hB zTzo(6GY{*(n_@C4t>6nsF~#Hz+bR?)<$8^@R;v2Z4k+?cA*F4WA!P%b?XMb~mv9)3 zK0|)3*c(T&9qMn(e={~v5)bV-;q3NZM20!Dw_Bi0kzRT5h2pksBg19x?Rb`iR?bpV ziUI9MgXMoyj+&pAxr;-pqpP~%+^TeRk2!4Pr60d*wY>Do#+^#4?QE^=ZmR5#nyM#< z>@{peOhjx%T>L=OCoMaR(ktf{1=Xu-Y$|GW7Iw%twj*k_*SA(Wi+4frK)xWYGP^U{ zRbv>TNajV43m_TfH$&^S1aY(3nk>|8u&XY{Sr>O8t=~9>^&-|AYxW9dh8y>CKLT;( zX2(Ng?@>V>yB1}eQ==+!#sT>u z$Jbc$MXw)YUDr8(2f3ewq7P4OBxHtvwWc=SdGS!cfP-%buWa1lhm5R`0-tXvz;-S~5xTZcdy=!kUWF=GnXLxUAM!H7_H4^!eR z-srv9@FGTNe8ZSN(`Z;{1>C!krb5$?R?s?sY)f@HIA0@yKko+$^BN8~ zkW?myTM7@~uys&}zk8&;d!Thf*EH|$yCz_PUhdjy1;2r*ymSX$_fFWpkd_(ho+w*= zhFN`T_R%nz59AWNVtIeQXodMF;8ZTIerB!l7ww>pqHVEkzWoYH-R~COv=}fTvE_Ec zU2Ef?@b$0QDe}a6%-P?%hlXdKz$o+QP_{shN5hU!^;{!B-3<1zd!Ya$<3hK6!2p9O zHU;@%I?zKbD>518V|Q~6IYEJV9&(l&b!GrP?9>bR67qcc5(ti0{W2@lS7W5!JaD_} z$};^W!47K)7W zC$(gg6G`JJj^40#Moyh8AwIB9%)>=v1pZP19}RfJK%`Kr0eDO1rj!r-d{!O^Njy!b z(f6*6%ZXqIE+PE-GtHY7A?ADwoRK;pS|0QB5fPQuDp!TWlTY6@2{-T*zs96_Kn%{# zbQ*CIv{k}_1*h^s7m~eS39?BMgyuJ_dcQ7b?A9{$6MZx|x(Sru`iUzSTnS2{Fr^4Z zlPs7O0NyG&sYFm1D`?ylkO)>Mo>8B2U@eMolJ%))%!gFnxn zxO0lsfM#q({?0AIcS`FplkAWNPl|sAk#;)inmEImY0ND?&$mHBX*ogG*gvB_JM0B6 z?C_gzW66%f^8@^W;Wbs>ma3OaI$7n`vm)EPq=bclyT_~5x zae%*%3glH*&UsIrYZ_^@!m)Z5riHpRnMC`()C|4qwe=@&mQL~sIf`>d#MYYVgvj-s z=tShwpb7G1|F1^%KJ48W@>cvA!ZGySbmjVqaQcOcS+WX}u}S=rH61>%3ShwRK(af$ zV7Sxujvzz>smqEG*+@3-c3HGm%v8v9>JV4*xeOZ93@|c~R4%v`WnP)R_Q1|aGwKzm zCPey*5i7S08QLg2k^$=p#fRvJS0m@nSZKxbki?Yp-{WqIhuo#kP!T!<5%d^mO^8)x zAf;?@C(OJ$AeVWsTvX5(V5^FUWY>Qt*Ehx)8F+uB;s&++sWNGTna(9GX(yplOh_)_ z5(Tzfo7_$s{=(IzfBllit{r&mr869Bd-Ffp$`?5W=7jB<;*yyfGQS3G8PNS?^aucf zF30167jnE8s@32j-duMW4tC(xQ}ct&$G=8|h2${rP>KCP?IJ# zcs$qm%MmrD?*r92bW96>vCD{k=na}zgR)Tsqe|rY5nflyYE;y9nutgPspS6T(zI67 zAQ+j(wSeUrZG5UMVP)5v$}0?#<#esR)dx63D@Z71M|oe6SDxDsR(kgh#5`GC2!aFW z+$kD}LnkCR+*|<(eRQmMj?l?hQM!L9gpE7KgE__{I%K~`^y3l95mtu*C+5+! zdvH$3+O%9AZ=?e_qny&WhRv1GSXXeU-<28-Q#ia?Q<(C%?8L?V(sl?ABuJeILf2u^ zqQ(a$XV#HK-j08(f8;)3;N}y;4U9v%yy1X$s>&aXoQF>22KXv7^@w{aw%4_!i1XRJ zmW$sLkv0Yap-+x?w88#RJb6JF2bt!!xe-!-)CZosE&q3+x-^wN`*)hZR1`yvnrm3{x6rlxp$m3~$a-R|{@C2@rmcW`*}PNGxJ_HTUAD&Do&xmHJF9KWnSG@M z_L;#pNUAlyULh2yUfK56j606m?~59x+a8&2>&ZpbCQ|W@G`+!jq1SE*hOE`UtkfBA zvqVZM$9jH2XnDcnFz2hSac7{auplzg)wHw8dKUWksGoX

W7=6%+~XG^v=LBwUUygpp=k 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

3lqIGUa#}Yu#c84LIK|V%_ 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%0AaXGwKDJ3wB> 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 zü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 dk;PuW3U!j6yN^I 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≠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 literal 0 HcmV?d00001 diff --git a/pluginInterfaceSupported.json b/pluginInterfaceSupported.json index 431c3b08..a5fdc62c 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": [ - "3.0" + "4.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 30cd77ba..505bb9fb 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -23,13 +23,14 @@ import com.zaxxer.hikari.pool.HikariPool; import io.supertokens.pluginInterface.*; import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.authRecipe.LoginMethod; +import io.supertokens.pluginInterface.authRecipe.sqlStorage.AuthRecipeSQLStorage; import io.supertokens.pluginInterface.dashboard.DashboardSearchTags; import io.supertokens.pluginInterface.dashboard.DashboardSessionInfo; import io.supertokens.pluginInterface.dashboard.DashboardUser; import io.supertokens.pluginInterface.dashboard.exceptions.UserIdNotFoundException; import io.supertokens.pluginInterface.dashboard.sqlStorage.DashboardSQLStorage; 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; @@ -55,6 +56,7 @@ 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.multitenancy.sqlStorage.MultitenancySQLStorage; import io.supertokens.pluginInterface.passwordless.PasswordlessCode; import io.supertokens.pluginInterface.passwordless.PasswordlessDevice; import io.supertokens.pluginInterface.passwordless.exception.*; @@ -77,6 +79,7 @@ import io.supertokens.pluginInterface.useridmapping.UserIdMappingStorage; import io.supertokens.pluginInterface.useridmapping.exception.UnknownSuperTokensUserIdException; import io.supertokens.pluginInterface.useridmapping.exception.UserIdMappingAlreadyExistsException; +import io.supertokens.pluginInterface.useridmapping.sqlStorage.UserIdMappingSQLStorage; import io.supertokens.pluginInterface.usermetadata.UserMetadataStorage; import io.supertokens.pluginInterface.usermetadata.sqlStorage.UserMetadataSQLStorage; import io.supertokens.pluginInterface.userroles.UserRolesStorage; @@ -98,12 +101,15 @@ import java.sql.Connection; import java.sql.SQLException; import java.sql.SQLTransactionRollbackException; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Set; public class Start implements SessionSQLStorage, EmailPasswordSQLStorage, EmailVerificationSQLStorage, ThirdPartySQLStorage, JWTRecipeSQLStorage, PasswordlessSQLStorage, UserMetadataSQLStorage, UserRolesSQLStorage, UserIdMappingStorage, - MultitenancyStorage, DashboardSQLStorage, TOTPSQLStorage, ActiveUsersStorage { + UserIdMappingSQLStorage, MultitenancyStorage, MultitenancySQLStorage, DashboardSQLStorage, TOTPSQLStorage, ActiveUsersStorage, 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. @@ -146,7 +152,8 @@ public STORAGE_TYPE getType() { } @Override - public void loadConfig(JsonObject configJson, Set logLevels, TenantIdentifier tenantIdentifier) throws InvalidConfigException { + public void loadConfig(JsonObject configJson, Set logLevels, TenantIdentifier tenantIdentifier) + throws InvalidConfigException { Config.loadConfig(this, configJson, logLevels, tenantIdentifier); } @@ -649,6 +656,17 @@ public void updateSessionInfo_Transaction(TenantIdentifier tenantIdentifier, Tra } } + @Override + public void deleteSessionsOfUser_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId) + throws StorageQueryException { + Connection sqlCon = (Connection) con.getConnection(); + try { + SessionQueries.deleteSessionsOfUser_Transaction(sqlCon, this, appIdentifier, userId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + @Override public void setKeyValue_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, String key, KeyValueInfo info) @@ -749,7 +767,8 @@ public boolean isUserIdBeingUsedInNonAuthRecipe(AppIdentifier appIdentifier, Str @TestOnly @Override - public void addInfoToNonAuthRecipesBasedOnUserId(TenantIdentifier tenantIdentifier, String className, String userId) throws StorageQueryException { + public void addInfoToNonAuthRecipesBasedOnUserId(TenantIdentifier tenantIdentifier, String className, String userId) + throws StorageQueryException { if (!isTesting) { throw new UnsupportedOperationException("This method is only for testing"); } @@ -818,7 +837,8 @@ public void addInfoToNonAuthRecipesBasedOnUserId(TenantIdentifier tenantIdentifi try { long now = System.currentTimeMillis(); TOTPQueries.insertUsedCode_Transaction(this, - (Connection) con.getConnection(), tenantIdentifier, new TOTPUsedCode(userId, "123456", true, 1000+now, now)); + (Connection) con.getConnection(), tenantIdentifier, + new TOTPUsedCode(userId, "123456", true, 1000 + now, now)); } catch (SQLException e) { throw new StorageTransactionLogicException(e); } @@ -852,7 +872,8 @@ public String[] getProtectedConfigsFromSuperTokensSaaSUsers() { } @Override - public UserInfo signUp(TenantIdentifier tenantIdentifier, String id, String email, String passwordHash, long timeJoined) + public AuthRecipeUserInfo signUp(TenantIdentifier tenantIdentifier, String id, String email, String passwordHash, + long timeJoined) throws StorageQueryException, DuplicateUserIdException, DuplicateEmailException, TenantOrAppNotFoundException { try { @@ -885,28 +906,12 @@ public UserInfo signUp(TenantIdentifier tenantIdentifier, String id, String emai } @Override - public void deleteEmailPasswordUser(AppIdentifier appIdentifier, String userId) throws StorageQueryException { - try { - EmailPasswordQueries.deleteUser(this, appIdentifier, userId); - } catch (StorageTransactionLogicException e) { - throw new StorageQueryException(e.actualException); - } - } - - @Override - public UserInfo getUserInfoUsingId(AppIdentifier appIdentifier, String id) throws StorageQueryException { - try { - return EmailPasswordQueries.getUserInfoUsingId(this, appIdentifier, id); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - - @Override - public UserInfo getUserInfoUsingEmail(TenantIdentifier tenantIdentifier, String email) + public void deleteEmailPasswordUser_Transaction(TransactionConnection con, AppIdentifier appIdentifier, + String userId, boolean deleteUserIdMappingToo) throws StorageQueryException { try { - return EmailPasswordQueries.getUserInfoUsingEmail(this, tenantIdentifier, email); + Connection sqlCon = (Connection) con.getConnection(); + EmailPasswordQueries.deleteUser_Transaction(sqlCon, this, appIdentifier, userId, deleteUserIdMappingToo); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -917,7 +922,7 @@ public void addPasswordResetToken(AppIdentifier appIdentifier, PasswordResetToke throws StorageQueryException, UnknownUserIdException, DuplicatePasswordResetTokenException { try { EmailPasswordQueries.addPasswordResetToken(this, appIdentifier, passwordResetTokenInfo.userId, - passwordResetTokenInfo.token, passwordResetTokenInfo.tokenExpiry); + passwordResetTokenInfo.token, passwordResetTokenInfo.tokenExpiry, passwordResetTokenInfo.email); } catch (SQLException e) { if (e instanceof PSQLException) { ServerErrorMessage serverMessage = ((PSQLException) e).getServerErrorMessage(); @@ -1009,18 +1014,6 @@ public void updateUsersEmail_Transaction(AppIdentifier appIdentifier, Transactio } } - @Override - public UserInfo getUserInfoUsingId_Transaction(AppIdentifier appIdentifier, TransactionConnection con, - String userId) - throws StorageQueryException { - Connection sqlCon = (Connection) con.getConnection(); - try { - return EmailPasswordQueries.getUserInfoUsingId_Transaction(this, sqlCon, appIdentifier, userId); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - @Override public void deleteExpiredEmailVerificationTokens() throws StorageQueryException { try { @@ -1090,12 +1083,14 @@ public void updateIsEmailVerified_Transaction(AppIdentifier appIdentifier, Trans } @Override - public void deleteEmailVerificationUserInfo(AppIdentifier appIdentifier, String userId) + public void deleteEmailVerificationUserInfo_Transaction(TransactionConnection con, AppIdentifier appIdentifier, + String userId) throws StorageQueryException { try { - EmailVerificationQueries.deleteUserInfo(this, appIdentifier, userId); - } catch (StorageTransactionLogicException e) { - throw new StorageQueryException(e.actualException); + Connection sqlCon = (Connection) con.getConnection(); + EmailVerificationQueries.deleteUserInfo_Transaction(sqlCon, this, appIdentifier, userId); + } catch (SQLException e) { + throw new StorageQueryException(e); } } @@ -1196,21 +1191,6 @@ public void deleteExpiredPasswordResetTokens() throws StorageQueryException { } } - @Override - public io.supertokens.pluginInterface.thirdparty.UserInfo getUserInfoUsingId_Transaction( - AppIdentifier appIdentifier, TransactionConnection con, - String thirdPartyId, - String thirdPartyUserId) - throws StorageQueryException { - Connection sqlCon = (Connection) con.getConnection(); - try { - return ThirdPartyQueries.getUserInfoUsingId_Transaction(this, sqlCon, appIdentifier, thirdPartyId, - thirdPartyUserId); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - @Override public void updateUserEmail_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String thirdPartyId, String thirdPartyUserId, @@ -1225,9 +1205,9 @@ public void updateUserEmail_Transaction(AppIdentifier appIdentifier, Transaction } @Override - public io.supertokens.pluginInterface.thirdparty.UserInfo signUp( + public AuthRecipeUserInfo signUp( TenantIdentifier tenantIdentifier, String id, String email, - io.supertokens.pluginInterface.thirdparty.UserInfo.ThirdParty thirdParty, long timeJoined) + LoginMethod.ThirdParty thirdParty, long timeJoined) throws StorageQueryException, io.supertokens.pluginInterface.thirdparty.exception.DuplicateUserIdException, DuplicateThirdPartyUserException, TenantOrAppNotFoundException { try { @@ -1268,44 +1248,12 @@ public io.supertokens.pluginInterface.thirdparty.UserInfo signUp( } @Override - public void deleteThirdPartyUser(AppIdentifier appIdentifier, String userId) throws StorageQueryException { - try { - ThirdPartyQueries.deleteUser(this, appIdentifier, userId); - } catch (StorageTransactionLogicException e) { - throw new StorageQueryException(e.actualException); - } - } - - @Override - public io.supertokens.pluginInterface.thirdparty.UserInfo getThirdPartyUserInfoUsingId( - TenantIdentifier tenantIdentifier, String thirdPartyId, - String thirdPartyUserId) + public void deleteThirdPartyUser_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId, + boolean deleteUserIdMappingToo) throws StorageQueryException { try { - return ThirdPartyQueries.getThirdPartyUserInfoUsingId(this, tenantIdentifier, thirdPartyId, - thirdPartyUserId); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - - @Override - public io.supertokens.pluginInterface.thirdparty.UserInfo getThirdPartyUserInfoUsingId(AppIdentifier appIdentifier, - String id) - throws StorageQueryException { - try { - return ThirdPartyQueries.getThirdPartyUserInfoUsingId(this, appIdentifier, id); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - - @Override - public io.supertokens.pluginInterface.thirdparty.UserInfo[] getThirdPartyUsersByEmail( - TenantIdentifier tenantIdentifier, @NotNull String email) - throws StorageQueryException { - try { - return ThirdPartyQueries.getThirdPartyUsersByEmail(this, tenantIdentifier, email); + Connection sqlCon = (Connection) con.getConnection(); + ThirdPartyQueries.deleteUser_Transaction(sqlCon, this, appIdentifier, userId, deleteUserIdMappingToo); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1392,9 +1340,11 @@ public int countUsersEnabledTotpAndActiveSince(AppIdentifier appIdentifier, long } @Override - public void deleteUserActive(AppIdentifier appIdentifier, String userId) throws StorageQueryException { + public void deleteUserActive_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId) + throws StorageQueryException { try { - ActiveUsersQueries.deleteUserActive(this, appIdentifier, userId); + Connection sqlCon = (Connection) con.getConnection(); + ActiveUsersQueries.deleteUserActive_Transaction(sqlCon, this, appIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1410,6 +1360,57 @@ public boolean doesUserIdExist(TenantIdentifier tenantIdentifier, String userId) } } + @Override + public AuthRecipeUserInfo getPrimaryUserById(AppIdentifier appIdentifier, String userId) + throws StorageQueryException { + try { + return GeneralQueries.getPrimaryUserInfoForUserId(this, appIdentifier, userId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public String getPrimaryUserIdStrForUserId(AppIdentifier appIdentifier, String userId) + throws StorageQueryException { + try { + return GeneralQueries.getPrimaryUserIdStrForUserId(this, appIdentifier, userId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public AuthRecipeUserInfo[] listPrimaryUsersByEmail(TenantIdentifier tenantIdentifier, String email) + throws StorageQueryException { + try { + return GeneralQueries.listPrimaryUsersByEmail(this, tenantIdentifier, email); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public AuthRecipeUserInfo[] listPrimaryUsersByPhoneNumber(TenantIdentifier tenantIdentifier, String phoneNumber) + throws StorageQueryException { + try { + return GeneralQueries.listPrimaryUsersByPhoneNumber(this, tenantIdentifier, phoneNumber); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public AuthRecipeUserInfo getPrimaryUserByThirdPartyInfo(TenantIdentifier tenantIdentifier, String thirdPartyId, + String thirdPartyUserId) throws StorageQueryException { + try { + return GeneralQueries.getPrimaryUserByThirdPartyInfo(this, tenantIdentifier, thirdPartyId, + thirdPartyUserId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + @Override public List getJWTSigningKeys_Transaction(AppIdentifier appIdentifier, TransactionConnection con) @@ -1598,7 +1599,8 @@ 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 { @@ -1716,9 +1718,11 @@ public void createCode(TenantIdentifier tenantIdentifier, PasswordlessCode code) } @Override - public io.supertokens.pluginInterface.passwordless.UserInfo createUser(TenantIdentifier tenantIdentifier, - String id, @javax.annotation.Nullable String email, - @javax.annotation.Nullable String phoneNumber, long timeJoined) + public AuthRecipeUserInfo createUser(TenantIdentifier tenantIdentifier, + String id, + @javax.annotation.Nullable String email, + @javax.annotation.Nullable + String phoneNumber, long timeJoined) throws StorageQueryException, DuplicateEmailException, DuplicatePhoneNumberException, DuplicateUserIdException, TenantOrAppNotFoundException { @@ -1772,12 +1776,14 @@ public io.supertokens.pluginInterface.passwordless.UserInfo createUser(TenantIde } @Override - public void deletePasswordlessUser(AppIdentifier appIdentifier, String userId) throws + public void deletePasswordlessUser_Transaction(TransactionConnection con, AppIdentifier appIdentifier, + String userId, boolean deleteUserIdMappingToo) throws StorageQueryException { try { - PasswordlessQueries.deleteUser(this, appIdentifier, userId); - } catch (StorageTransactionLogicException e) { - throw new StorageQueryException(e.actualException); + Connection sqlCon = (Connection) con.getConnection(); + PasswordlessQueries.deleteUser_Transaction(sqlCon, this, appIdentifier, userId, deleteUserIdMappingToo); + } catch (SQLException e) { + throw new StorageQueryException(e); } } @@ -1852,37 +1858,6 @@ public PasswordlessCode getCodeByLinkCodeHash(TenantIdentifier tenantIdentifier, } } - @Override - public io.supertokens.pluginInterface.passwordless.UserInfo getUserById(AppIdentifier appIdentifier, - String userId) - throws StorageQueryException { - try { - return PasswordlessQueries.getUserById(this, appIdentifier, userId); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - - @Override - public io.supertokens.pluginInterface.passwordless.UserInfo getUserByEmail(TenantIdentifier tenantIdentifier, String email) - throws StorageQueryException { - try { - return PasswordlessQueries.getUserByEmail(this, tenantIdentifier, email); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - - @Override - public io.supertokens.pluginInterface.passwordless.UserInfo getUserByPhoneNumber(TenantIdentifier tenantIdentifier, String phoneNumber) - throws StorageQueryException { - try { - return PasswordlessQueries.getUserByPhoneNumber(this, tenantIdentifier, phoneNumber); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - @Override public JsonObject getUserMetadata(AppIdentifier appIdentifier, String userId) throws StorageQueryException { try { @@ -1905,7 +1880,8 @@ 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 { @@ -1924,6 +1900,17 @@ public int setUserMetadata_Transaction(AppIdentifier appIdentifier, TransactionC } } + @Override + public int deleteUserMetadata_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId) + throws StorageQueryException { + try { + Connection sqlCon = (Connection) con.getConnection(); + return UserMetadataQueries.deleteUserMetadata_Transaction(sqlCon, this, appIdentifier, userId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + @Override public int deleteUserMetadata(AppIdentifier appIdentifier, String userId) throws StorageQueryException { try { @@ -2046,17 +2033,20 @@ public int deleteAllRolesForUser(TenantIdentifier tenantIdentifier, String userI } @Override - public void deleteAllRolesForUser(AppIdentifier appIdentifier, String userId) throws + public void deleteAllRolesForUser_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId) + throws StorageQueryException { try { - UserRolesQueries.deleteAllRolesForUser(this, appIdentifier, userId); + Connection sqlCon = (Connection) con.getConnection(); + UserRolesQueries.deleteAllRolesForUser_Transaction(sqlCon, this, appIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } } @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(); @@ -2114,7 +2104,8 @@ 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 { @@ -2126,7 +2117,8 @@ 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 { @@ -2138,7 +2130,8 @@ 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); @@ -2183,7 +2176,8 @@ 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, @@ -2197,7 +2191,8 @@ public boolean deleteUserIdMapping(AppIdentifier appIdentifier, String userId, b } @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, @@ -2332,6 +2327,7 @@ public boolean deleteTenantInfoInBaseStorage(TenantIdentifier tenantIdentifier) 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)); @@ -2343,67 +2339,57 @@ public TenantConfig[] getAllTenants() throws StorageQueryException { } @Override - public boolean addUserIdToTenant(TenantIdentifier tenantIdentifier, String userId) + public boolean addUserIdToTenant_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, String userId) throws TenantOrAppNotFoundException, UnknownUserIdException, StorageQueryException, DuplicateEmailException, DuplicateThirdPartyUserException, DuplicatePhoneNumberException { + Connection sqlCon = (Connection) con.getConnection(); 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()); - } + String recipeId = GeneralQueries.getRecipeIdForUser_Transaction(this, sqlCon, tenantIdentifier, + userId); - 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!"); - } + if (recipeId == null) { + throw new UnknownUserIdException(); + } - 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(); + 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!"); + } - 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(); - } + sqlCon.commit(); + return added; + } catch (SQLException throwables) { + PostgreSQLConfig config = Config.getConfig(this); + ServerErrorMessage serverErrorMessage = ((PSQLException) throwables).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; + if (isForeignKeyConstraintError(serverErrorMessage, config.getUsersTable(), "tenant_id")) { + throw new TenantOrAppNotFoundException(tenantIdentifier); } - throw new StorageQueryException(e.actualException); + 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(throwables); } } @@ -2424,11 +2410,14 @@ public boolean removeUserIdFromTenant(TenantIdentifier tenantIdentifier, String boolean removed; if (recipeId.equals("emailpassword")) { - removed = EmailPasswordQueries.removeUserIdFromTenant_Transaction(this, sqlCon, tenantIdentifier, userId); + removed = EmailPasswordQueries.removeUserIdFromTenant_Transaction(this, sqlCon, + tenantIdentifier, userId); } else if (recipeId.equals("thirdparty")) { - removed = ThirdPartyQueries.removeUserIdFromTenant_Transaction(this, sqlCon, tenantIdentifier, userId); + removed = ThirdPartyQueries.removeUserIdFromTenant_Transaction(this, sqlCon, tenantIdentifier, + userId); } else if (recipeId.equals("passwordless")) { - removed = PasswordlessQueries.removeUserIdFromTenant_Transaction(this, sqlCon, tenantIdentifier, userId); + removed = PasswordlessQueries.removeUserIdFromTenant_Transaction(this, sqlCon, tenantIdentifier, + userId); } else { throw new IllegalStateException("Should never come here!"); } @@ -2446,11 +2435,12 @@ public boolean removeUserIdFromTenant(TenantIdentifier tenantIdentifier, String throw (StorageQueryException) e.actualException; } throw new StorageQueryException(e.actualException); - } + } } @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) { @@ -2500,7 +2490,8 @@ 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); @@ -2510,7 +2501,8 @@ 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 { Connection sqlCon = (Connection) con.getConnection(); @@ -2815,8 +2807,182 @@ public String[] getAllTablesInTheDatabaseThatHasDataForAppId(String appId) throw } } + @Override + public AuthRecipeUserInfo getPrimaryUserById_Transaction(AppIdentifier appIdentifier, TransactionConnection con, + String userId) + throws StorageQueryException { + try { + Connection sqlCon = (Connection) con.getConnection(); + return GeneralQueries.getPrimaryUserInfoForUserId_Transaction(this, sqlCon, appIdentifier, userId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public AuthRecipeUserInfo[] listPrimaryUsersByEmail_Transaction(AppIdentifier appIdentifier, + TransactionConnection con, String email) + throws StorageQueryException { + try { + Connection sqlCon = (Connection) con.getConnection(); + return GeneralQueries.listPrimaryUsersByEmail_Transaction(this, sqlCon, appIdentifier, email); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public AuthRecipeUserInfo[] listPrimaryUsersByPhoneNumber_Transaction(AppIdentifier appIdentifier, + TransactionConnection con, + String phoneNumber) + throws StorageQueryException { + try { + Connection sqlCon = (Connection) con.getConnection(); + return GeneralQueries.listPrimaryUsersByPhoneNumber_Transaction(this, sqlCon, appIdentifier, + phoneNumber); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public AuthRecipeUserInfo[] listPrimaryUsersByThirdPartyInfo(AppIdentifier appIdentifier, + String thirdPartyId, + String thirdPartyUserId) + throws StorageQueryException { + try { + return GeneralQueries.listPrimaryUsersByThirdPartyInfo(this, appIdentifier, + thirdPartyId, thirdPartyUserId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public AuthRecipeUserInfo[] listPrimaryUsersByThirdPartyInfo_Transaction(AppIdentifier appIdentifier, + TransactionConnection con, + String thirdPartyId, + String thirdPartyUserId) + throws StorageQueryException { + try { + Connection sqlCon = (Connection) con.getConnection(); + return GeneralQueries.listPrimaryUsersByThirdPartyInfo_Transaction(this, sqlCon, appIdentifier, + thirdPartyId, thirdPartyUserId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public void makePrimaryUser_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId) + throws StorageQueryException { + try { + Connection sqlCon = (Connection) con.getConnection(); + // we do not bother returning if a row was updated here or not, cause it's happening + // in a transaction anyway. + GeneralQueries.makePrimaryUser_Transaction(this, sqlCon, appIdentifier, userId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public void linkAccounts_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String recipeUserId, + String primaryUserId) throws StorageQueryException { + try { + Connection sqlCon = (Connection) con.getConnection(); + // we do not bother returning if a row was updated here or not, cause it's happening + // in a transaction anyway. + GeneralQueries.linkAccounts_Transaction(this, sqlCon, appIdentifier, recipeUserId, primaryUserId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public void unlinkAccounts_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String primaryUserId, String recipeUserId) + throws StorageQueryException { + try { + Connection sqlCon = (Connection) con.getConnection(); + // we do not bother returning if a row was updated here or not, cause it's happening + // in a transaction anyway. + GeneralQueries.unlinkAccounts_Transaction(this, sqlCon, appIdentifier, primaryUserId, recipeUserId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public boolean doesUserIdExist_Transaction(TransactionConnection con, AppIdentifier appIdentifier, + String externalUserId) throws StorageQueryException { + try { + Connection sqlCon = (Connection) con.getConnection(); + return GeneralQueries.doesUserIdExist_Transaction(this, sqlCon, appIdentifier, externalUserId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public boolean checkIfUsesAccountLinking(AppIdentifier appIdentifier) throws StorageQueryException { + try { + return GeneralQueries.checkIfUsesAccountLinking(this, appIdentifier); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public int countUsersThatHaveMoreThanOneLoginMethodAndActiveSince(AppIdentifier appIdentifier, long sinceTime) throws StorageQueryException { + try { + return ActiveUsersQueries.countUsersActiveSinceAndHasMoreThanOneLoginMethod(this, appIdentifier, sinceTime); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public int getUsersCountWithMoreThanOneLoginMethod(AppIdentifier appIdentifier) throws StorageQueryException { + try { + return GeneralQueries.getUsersCountWithMoreThanOneLoginMethod(this, appIdentifier); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + @TestOnly public Thread getMainThread() { return mainThread; } + + @Override + public UserIdMapping getUserIdMapping_Transaction(TransactionConnection con, AppIdentifier appIdentifier, + String userId, boolean isSuperTokensUserId) + throws StorageQueryException { + try { + Connection sqlCon = (Connection) con.getConnection(); + if (isSuperTokensUserId) { + return UserIdMappingQueries.getuseraIdMappingWithSuperTokensUserId_Transaction(this, sqlCon, appIdentifier, + userId); + } + + return UserIdMappingQueries.getUserIdMappingWithExternalUserId_Transaction(this, sqlCon, appIdentifier, userId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public UserIdMapping[] getUserIdMapping_Transaction(TransactionConnection con, AppIdentifier appIdentifier, + String userId) throws StorageQueryException { + try { + Connection sqlCon = (Connection) con.getConnection(); + return UserIdMappingQueries.getUserIdMappingWithEitherSuperTokensUserIdOrExternalUserId_Transaction(this, + sqlCon, + appIdentifier, + userId); + } 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 52508166..d40c08f1 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java @@ -1,14 +1,14 @@ package io.supertokens.storage.postgresql.queries; -import java.math.BigInteger; -import java.sql.SQLException; - +import io.supertokens.pluginInterface.exceptions.StorageQueryException; 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 java.sql.Connection; +import java.sql.SQLException; + import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.update; @@ -21,9 +21,10 @@ static String getQueryToCreateUserLastActiveTable(Start start) { + "user_id VARCHAR(128)," + "last_active_time BIGINT," + "PRIMARY KEY(app_id, user_id)," - + "CONSTRAINT " + Utils.getConstraintName(schema, Config.getConfig(start).getUserLastActiveTable(), "app_id", "fkey") + + "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" + + " REFERENCES " + Config.getConfig(start).getAppsTable() + " (app_id) ON DELETE CASCADE" + ");"; } @@ -32,7 +33,8 @@ static String getQueryToCreateAppIdIndexForUserLastActiveTable(Start start) { + Config.getConfig(start).getUserLastActiveTable() + "(app_id);"; } - public static int countUsersActiveSince(Start start, AppIdentifier appIdentifier, 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 app_id = ? AND last_active_time >= ?"; @@ -47,7 +49,30 @@ public static int countUsersActiveSince(Start start, AppIdentifier appIdentifier }); } - public static int countUsersEnabledTotp(Start start, AppIdentifier appIdentifier) throws SQLException, StorageQueryException { + public static int countUsersActiveSinceAndHasMoreThanOneLoginMethod(Start start, AppIdentifier appIdentifier, long sinceTime) + throws SQLException, StorageQueryException { + 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() + + " WHERE primary_or_recipe_user_id IN (" + + " SELECT user_id FROM " + Config.getConfig(start).getUserLastActiveTable() + + " WHERE app_id = ? AND last_active_time >= ?" + + " )" + + " GROUP BY app_id, primary_or_recipe_user_id" + + ") uc WHERE num_login_methods > 1"; + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setLong(2, sinceTime); + }, result -> { + if (result.next()) { + return result.getInt("c"); + } + return 0; + }); + } + + 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 = ?"; @@ -61,11 +86,13 @@ public static int countUsersEnabledTotp(Start start, AppIdentifier appIdentifier }); } - 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 >= ?"; + 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()); @@ -78,9 +105,12 @@ public static int countUsersEnabledTotpAndActiveSince(Start start, AppIdentifier }); } - public static int updateUserLastActive(Start start, AppIdentifier appIdentifier, 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() - + "(app_id, user_id, last_active_time) VALUES(?, ?, ?) ON CONFLICT(app_id, 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 -> { @@ -111,12 +141,13 @@ public static Long getLastActiveByUserId(Start start, AppIdentifier appIdentifie } } - public static void deleteUserActive(Start start, AppIdentifier appIdentifier, String userId) + public static void deleteUserActive_Transaction(Connection con, 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 -> { + update(con, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); pst.setString(2, userId); }); 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 8b893162..55bb51c4 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java @@ -17,8 +17,11 @@ package io.supertokens.storage.postgresql.queries; import io.supertokens.pluginInterface.RowMapper; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.authRecipe.LoginMethod; 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.UnknownUserIdException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; @@ -32,6 +35,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.*; +import java.util.stream.Collectors; import static io.supertokens.pluginInterface.RECIPE_ID.EMAIL_PASSWORD; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; @@ -52,7 +56,8 @@ static String getQueryToCreateUsersTable(Start start) { + "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," + + " 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)" + ");"; @@ -74,7 +79,8 @@ static String getQueryToCreateEmailPasswordUserToTenantTable(Start start) { + " 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" + + " REFERENCES " + Config.getConfig(start).getUsersTable() + + "(app_id, tenant_id, user_id) ON DELETE CASCADE" + ");"; // @formatter:on } @@ -87,13 +93,15 @@ static String getQueryToCreatePasswordResetTokensTable(Start start) { + "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," + + " CONSTRAINT " + Utils.getConstraintName(schema, passwordResetTokensTable, "token", "key") + + " UNIQUE," + + "email VARCHAR(256)," // nullable cause of backwards compatibility. + "token_expiry BIGINT NOT NULL," + "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)" + + " REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable() + "(app_id, user_id)" + " ON DELETE CASCADE ON UPDATE CASCADE" + ");"; // @formatter:on @@ -115,7 +123,8 @@ 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, AppIdentifier appIdentifier, 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 app_id = ? AND user_id = ?"; @@ -127,7 +136,8 @@ public static void updateUsersPassword_Transaction(Start start, Connection con, }); } - public static void updateUsersEmail_Transaction(Start start, Connection con, AppIdentifier appIdentifier, 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() @@ -151,10 +161,12 @@ public static void updateUsersEmail_Transaction(Start start, Connection con, App } } - public static void deleteAllPasswordResetTokensForUser_Transaction(Start start, Connection con, AppIdentifier appIdentifier, 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 app_id = ? AND user_id = ?"; + String QUERY = + "DELETE FROM " + getConfig(start).getPasswordResetTokensTable() + " WHERE app_id = ? AND user_id = ?"; update(con, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); @@ -165,8 +177,9 @@ public static void deleteAllPasswordResetTokensForUser_Transaction(Start start, 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 app_id = ? AND user_id = ?"; + String QUERY = + "SELECT user_id, token, token_expiry, email FROM " + getConfig(start).getPasswordResetTokensTable() + + " WHERE app_id = ? AND user_id = ?"; return execute(start, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); @@ -189,8 +202,9 @@ public static PasswordResetTokenInfo[] getAllPasswordResetTokenInfoForUser_Trans String userId) throws SQLException, StorageQueryException { - String QUERY = "SELECT user_id, token, token_expiry FROM " + getConfig(start).getPasswordResetTokensTable() - + " WHERE app_id = ? AND user_id = ? FOR UPDATE"; + String QUERY = + "SELECT user_id, token, token_expiry, email FROM " + getConfig(start).getPasswordResetTokensTable() + + " WHERE app_id = ? AND user_id = ? FOR UPDATE"; return execute(con, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); @@ -208,28 +222,12 @@ public static PasswordResetTokenInfo[] getAllPasswordResetTokenInfoForUser_Trans }); } - public static UserInfo getUserInfoUsingId_Transaction(Start start, Connection con, AppIdentifier appIdentifier, - String id) + public static PasswordResetTokenInfo getPasswordResetTokenInfo(Start start, AppIdentifier appIdentifier, + String token) throws SQLException, StorageQueryException { - String QUERY = "SELECT user_id, email, password_hash, time_joined FROM " - + getConfig(start).getEmailPasswordUsersTable() - + " WHERE app_id = ? AND user_id = ? FOR UPDATE"; - UserInfoPartial userInfo = execute(con, QUERY, pst -> { - pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, id); - }, result -> { - if (result.next()) { - return UserInfoRowMapper.getInstance().mapOrThrow(result); - } - return null; - }); - return userInfoWithTenantIds_transaction(start, con, appIdentifier, userInfo); - } - - 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 app_id = ? AND token = ?"; + String QUERY = + "SELECT user_id, token, token_expiry, email FROM " + getConfig(start).getPasswordResetTokensTable() + + " WHERE app_id = ? AND token = ?"; return execute(start, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); pst.setString(2, token); @@ -241,43 +239,62 @@ public static PasswordResetTokenInfo getPasswordResetTokenInfo(Start start, AppI }); } - public static void addPasswordResetToken(Start start, AppIdentifier appIdentifier, String userId, String tokenHash, long expiry) + public static void addPasswordResetToken(Start start, AppIdentifier appIdentifier, String userId, String tokenHash, + long expiry, String email) throws SQLException, StorageQueryException { - String QUERY = "INSERT INTO " + getConfig(start).getPasswordResetTokensTable() - + "(app_id, user_id, token, token_expiry)" + " VALUES(?, ?, ?, ?)"; + if (email != null) { + String QUERY = "INSERT INTO " + getConfig(start).getPasswordResetTokensTable() + + "(app_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); - }); + 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); + }); + } else { + String QUERY = "INSERT INTO " + getConfig(start).getPasswordResetTokensTable() + + "(app_id, user_id, token, token_expiry)" + " VALUES(?, ?, ?, ?)"; + + update(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + pst.setString(3, tokenHash); + pst.setLong(4, expiry); + }); + } } - public static UserInfo signUp(Start start, TenantIdentifier tenantIdentifier, String userId, String email, String passwordHash, long timeJoined) + public static AuthRecipeUserInfo signUp(Start start, TenantIdentifier tenantIdentifier, String userId, String email, + String passwordHash, long timeJoined) throws StorageQueryException, StorageTransactionLogicException { return 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, recipe_id)" + " VALUES(?, ?, ?)"; + + "(app_id, user_id, primary_or_recipe_user_id, recipe_id)" + " VALUES(?, ?, ?, ?)"; update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, userId); - pst.setString(3, EMAIL_PASSWORD.toString()); + pst.setString(3, userId); + pst.setString(4, EMAIL_PASSWORD.toString()); }); } { // all_auth_recipe_users String QUERY = "INSERT INTO " + getConfig(start).getUsersTable() - + "(app_id, tenant_id, user_id, recipe_id, time_joined)" + " VALUES(?, ?, ?, ?, ?)"; + + "(app_id, tenant_id, user_id, primary_or_recipe_user_id, recipe_id, time_joined, primary_or_recipe_user_time_joined)" + + " VALUES(?, ?, ?, ?, ?, ?, ?)"; 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, timeJoined); + pst.setString(4, userId); + pst.setString(5, EMAIL_PASSWORD.toString()); + pst.setLong(6, timeJoined); + pst.setLong(7, timeJoined); }); } @@ -306,57 +323,62 @@ public static UserInfo signUp(Start start, TenantIdentifier tenantIdentifier, St }); } - UserInfo userInfo = userInfoWithTenantIds_transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), new UserInfoPartial(userId, email, passwordHash, timeJoined)); - + UserInfoPartial userInfo = new UserInfoPartial(userId, email, passwordHash, timeJoined); + fillUserInfoWithTenantIds_transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), userInfo); + fillUserInfoWithVerified_transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), userInfo); sqlCon.commit(); - return userInfo; + return AuthRecipeUserInfo.create(userId, false, userInfo.toLoginMethod()); } catch (SQLException throwables) { throw new StorageTransactionLogicException(throwables); } }); } - 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).getAppIdToUserIdTable() - + " WHERE app_id = ? AND user_id = ?"; - - update(sqlCon, QUERY, pst -> { - pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, userId); - }); - } + public static void deleteUser_Transaction(Connection sqlCon, Start start, AppIdentifier appIdentifier, + String userId, boolean deleteUserIdMappingToo) + throws StorageQueryException, SQLException { + if (deleteUserIdMappingToo) { + String QUERY = "DELETE FROM " + getConfig(start).getAppIdToUserIdTable() + + " WHERE app_id = ? AND user_id = ?"; - sqlCon.commit(); - } catch (SQLException throwables) { - throw new StorageTransactionLogicException(throwables); + update(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); + } else { + { + String QUERY = "DELETE FROM " + getConfig(start).getUsersTable() + + " WHERE app_id = ? AND user_id = ?"; + update(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); } - 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 = ?"; + { + String QUERY = "DELETE FROM " + getConfig(start).getEmailPasswordUsersTable() + + " WHERE app_id = ? AND user_id = ?"; + update(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); + } - UserInfoPartial userInfo = execute(start, QUERY.toString(), pst -> { - pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, id); - }, result -> { - if (result.next()) { - return UserInfoRowMapper.getInstance().mapOrThrow(result); + { + String QUERY = "DELETE FROM " + getConfig(start).getPasswordResetTokensTable() + + " WHERE app_id = ? AND user_id = ?"; + update(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); } - return null; - }); - return userInfoWithTenantIds(start, appIdentifier, userInfo); + } } - 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 + private static UserInfoPartial getUserInfoUsingId_Transaction(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 = ?"; @@ -371,29 +393,55 @@ public static UserInfoPartial getUserInfoUsingId(Start start, Connection sqlCon, }); } - public static List getUsersInfoUsingIdList(Start start, AppIdentifier appIdentifier, List ids) + public static List getUsersInfoUsingIdList(Start start, Set ids, + AppIdentifier appIdentifier) 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, email, password_hash, time_joined " - + "FROM " + getConfig(start).getEmailPasswordUsersTable()); - QUERY.append(" WHERE app_id = ? AND user_id IN ("); - for (int i = 0; i < ids.size(); i++) { - - QUERY.append("?"); - if (i != ids.size() - 1) { - // not the last element - QUERY.append(","); + String QUERY = "SELECT user_id, email, password_hash, time_joined " + + "FROM " + getConfig(start).getEmailPasswordUsersTable() + + " WHERE user_id IN (" + Utils.generateCommaSeperatedQuestionMarks(ids.size()) + + " ) AND app_id = ?"; + + List userInfos = execute(start, QUERY, pst -> { + int index = 1; + for (String id : ids) { + pst.setString(index, id); + index++; } - } - QUERY.append(")"); + pst.setString(index, appIdentifier.getAppId()); + }, result -> { + List finalResult = new ArrayList<>(); + while (result.next()) { + finalResult.add(UserInfoRowMapper.getInstance().mapOrThrow(result)); + } + return finalResult; + }); + fillUserInfoWithTenantIds(start, appIdentifier, userInfos); + fillUserInfoWithVerified(start, appIdentifier, userInfos); + return userInfos.stream().map(UserInfoPartial::toLoginMethod) + .collect(Collectors.toList()); + } + return Collections.emptyList(); + } - List userInfos = execute(start, QUERY.toString(), pst -> { - pst.setString(1, appIdentifier.getAppId()); - for (int i = 0; i < ids.size(); i++) { - // i+2 cause this starts with 1 and not 0, and 1 is appId - pst.setString(i + 2, ids.get(i)); + public static List getUsersInfoUsingIdList_Transaction(Start start, Connection con, Set ids, + AppIdentifier appIdentifier) + throws SQLException, StorageQueryException { + if (ids.size() > 0) { + // No need to filter based on tenantId because the id list is already filtered for a tenant + String QUERY = "SELECT user_id, email, password_hash, time_joined " + + "FROM " + getConfig(start).getEmailPasswordUsersTable() + + " WHERE user_id IN (" + Utils.generateCommaSeperatedQuestionMarks(ids.size()) + + " ) AND app_id = ?"; + + List userInfos = execute(con, QUERY, pst -> { + int index = 1; + for (String id : ids) { + pst.setString(index, id); + index++; } + pst.setString(index, appIdentifier.getAppId()); }, result -> { List finalResult = new ArrayList<>(); while (result.next()) { @@ -401,55 +449,111 @@ public static List getUsersInfoUsingIdList(Start start, AppIdentifier } return finalResult; }); - return userInfoWithTenantIds(start, appIdentifier, userInfos); + fillUserInfoWithTenantIds_transaction(start, con, appIdentifier, userInfos); + fillUserInfoWithVerified_transaction(start, con, appIdentifier, userInfos); + return userInfos.stream().map(UserInfoPartial::toLoginMethod) + .collect(Collectors.toList()); } return Collections.emptyList(); } + public static String lockEmail_Transaction(Start start, Connection con, + AppIdentifier appIdentifier, + String email) + throws StorageQueryException, SQLException { + String QUERY = "SELECT user_id FROM " + getConfig(start).getEmailPasswordUsersTable() + + " WHERE app_id = ? AND email = ? FOR UPDATE"; - public static UserInfo getUserInfoUsingEmail(Start start, TenantIdentifier tenantIdentifier, String email) throws StorageQueryException, SQLException { - 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 = ?"; + return execute(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, email); + }, result -> { + if (result.next()) { + return result.getString("user_id"); + } + return null; + }); + } + + public static String getPrimaryUserIdUsingEmail(Start start, TenantIdentifier tenantIdentifier, + String email) + throws StorageQueryException, SQLException { + String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id " + + "FROM " + getConfig(start).getEmailPasswordUserToTenantTable() + " AS ep" + + " JOIN " + getConfig(start).getUsersTable() + " AS all_users" + + " ON ep.app_id = all_users.app_id AND ep.user_id = all_users.user_id" + + " WHERE ep.app_id = ? AND ep.tenant_id = ? AND ep.email = ?"; - UserInfoPartial userInfo = execute(start, QUERY, pst -> { + 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 result.getString("user_id"); } return null; }); - return userInfoWithTenantIds(start, tenantIdentifier.toAppIdentifier(), userInfo); + } + + public static List getPrimaryUserIdsUsingEmail_Transaction(Start start, Connection con, AppIdentifier appIdentifier, + String email) + throws StorageQueryException, SQLException { + String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id " + + "FROM " + getConfig(start).getEmailPasswordUsersTable() + " AS ep" + + " JOIN " + getConfig(start).getAppIdToUserIdTable() + " AS all_users" + + " ON ep.app_id = all_users.app_id AND ep.user_id = all_users.user_id" + + " WHERE ep.app_id = ? AND ep.email = ?"; + + return execute(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, email); + }, result -> { + List userIds = new ArrayList<>(); + while (result.next()) { + userIds.add(result.getString("user_id")); + } + return userIds; + }); } public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId) - throws SQLException, StorageQueryException { - UserInfoPartial userInfo = EmailPasswordQueries.getUserInfoUsingId(start, sqlCon, + throws SQLException, StorageQueryException, UnknownUserIdException { + UserInfoPartial userInfo = EmailPasswordQueries.getUserInfoUsingId_Transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), userId); + if (userInfo == null) { + throw new UnknownUserIdException(); + } + + GeneralQueries.AccountLinkingInfo accountLinkingInfo = GeneralQueries.getAccountLinkingInfo_Transaction(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"; + + "(app_id, tenant_id, user_id, primary_or_recipe_user_id, is_linked_or_is_a_primary_user, recipe_id, time_joined, primary_or_recipe_user_time_joined)" + + " VALUES(?, ?, ?, ?, ?, ?, ?, ?)" + " ON CONFLICT DO NOTHING"; + GeneralQueries.AccountLinkingInfo finalAccountLinkingInfo = accountLinkingInfo; + 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); + pst.setString(4, finalAccountLinkingInfo.primaryUserId); + pst.setBoolean(5, finalAccountLinkingInfo.isLinked); + pst.setString(6, EMAIL_PASSWORD.toString()); + pst.setLong(7, userInfo.timeJoined); + pst.setLong(8, userInfo.timeJoined); }); + + GeneralQueries.updateTimeJoinedForPrimaryUser_Transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), finalAccountLinkingInfo.primaryUserId); } { // emailpassword_user_to_tenant String QUERY = "INSERT INTO " + getConfig(start).getEmailPasswordUserToTenantTable() + "(app_id, tenant_id, user_id, email)" - + " VALUES(?, ?, ?, ?) " + " ON CONFLICT DO NOTHING"; + + " VALUES(?, ?, ?, ?) " + " ON CONFLICT ON CONSTRAINT " + + Utils.getConstraintName(Config.getConfig(start).getTableSchema(), getConfig(start).getEmailPasswordUserToTenantTable(), null, "pkey") + + " DO NOTHING"; int numRows = update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); @@ -462,7 +566,8 @@ public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlC } } - public static boolean removeUserIdFromTenant_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId) + 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() @@ -478,42 +583,103 @@ 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, AppIdentifier appIdentifier, UserInfoPartial userInfo) + private static UserInfoPartial fillUserInfoWithVerified_transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + UserInfoPartial userInfo) throws SQLException, StorageQueryException { if (userInfo == null) return null; - try (Connection con = ConnectionPool.getConnection(start)) { - return userInfoWithTenantIds_transaction(start, con, appIdentifier, Arrays.asList(userInfo)).get(0); + return fillUserInfoWithVerified_transaction(start, sqlCon, appIdentifier, List.of(userInfo)).get(0); + } + + private static List fillUserInfoWithVerified_transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + List userInfos) + throws SQLException, StorageQueryException { + List userIdsAndEmails = new ArrayList<>(); + for (UserInfoPartial userInfo : userInfos) { + userIdsAndEmails.add(new EmailVerificationQueries.UserIdAndEmail(userInfo.id, userInfo.email)); + } + List userIdsThatAreVerified = EmailVerificationQueries.isEmailVerified_transaction(start, sqlCon, + appIdentifier, + userIdsAndEmails); + Set verifiedUserIdsSet = new HashSet<>(userIdsThatAreVerified); + for (UserInfoPartial userInfo : userInfos) { + if (verifiedUserIdsSet.contains(userInfo.id)) { + userInfo.verified = true; + } else { + userInfo.verified = false; + } } + return userInfos; } - private static List userInfoWithTenantIds(Start start, AppIdentifier appIdentifier, List userInfos) + private static List fillUserInfoWithVerified(Start start, + AppIdentifier appIdentifier, + List userInfos) throws SQLException, StorageQueryException { - try (Connection con = ConnectionPool.getConnection(start)) { - return userInfoWithTenantIds_transaction(start, con, appIdentifier, userInfos); + List userIdsAndEmails = new ArrayList<>(); + for (UserInfoPartial userInfo : userInfos) { + userIdsAndEmails.add(new EmailVerificationQueries.UserIdAndEmail(userInfo.id, userInfo.email)); } + List userIdsThatAreVerified = EmailVerificationQueries.isEmailVerified(start, + appIdentifier, + userIdsAndEmails); + Set verifiedUserIdsSet = new HashSet<>(userIdsThatAreVerified); + for (UserInfoPartial userInfo : userInfos) { + if (verifiedUserIdsSet.contains(userInfo.id)) { + userInfo.verified = true; + } else { + userInfo.verified = false; + } + } + return userInfos; } - private static UserInfo userInfoWithTenantIds_transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, UserInfoPartial userInfo) + private static UserInfoPartial fillUserInfoWithTenantIds_transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + UserInfoPartial userInfo) throws SQLException, StorageQueryException { if (userInfo == null) return null; - return userInfoWithTenantIds_transaction(start, sqlCon, appIdentifier, Arrays.asList(userInfo)).get(0); + return fillUserInfoWithTenantIds_transaction(start, sqlCon, appIdentifier, Arrays.asList(userInfo)).get(0); } - private static List userInfoWithTenantIds_transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, List userInfos) + private static List fillUserInfoWithTenantIds_transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + 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, appIdentifier, userIds); - List result = new ArrayList<>(); + Map> tenantIdsForUserIds = GeneralQueries.getTenantIdsForUserIds_transaction(start, sqlCon, + appIdentifier, + 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]))); + userInfo.tenantIds = tenantIdsForUserIds.get(userInfo.id).toArray(new String[0]); } - return result; + return userInfos; + } + + private static List fillUserInfoWithTenantIds(Start start, + AppIdentifier appIdentifier, + 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(start, + appIdentifier, + userIds); + for (UserInfoPartial userInfo : userInfos) { + userInfo.tenantIds = tenantIdsForUserIds.get(userInfo.id).toArray(new String[0]); + } + + return userInfos; } private static class UserInfoPartial { @@ -521,6 +687,9 @@ private static class UserInfoPartial { public final long timeJoined; public final String email; public final String passwordHash; + public String[] tenantIds; + public Boolean verified; + public Boolean isPrimary; public UserInfoPartial(String id, String email, String passwordHash, long timeJoined) { this.id = id.trim(); @@ -528,6 +697,13 @@ public UserInfoPartial(String id, String email, String passwordHash, long timeJo this.email = email; this.passwordHash = passwordHash; } + + public LoginMethod toLoginMethod() { + assert (tenantIds != null); + assert (verified != null); + return new LoginMethod(id, timeJoined, verified, email, + passwordHash, tenantIds); + } } private static class PasswordResetRowMapper implements RowMapper { @@ -544,7 +720,7 @@ private static PasswordResetRowMapper getInstance() { public PasswordResetTokenInfo map(ResultSet result) throws StorageQueryException { try { return new PasswordResetTokenInfo(result.getString("user_id"), result.getString("token"), - result.getLong("token_expiry")); + result.getLong("token_expiry"), result.getString("email")); } catch (Exception 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 afe360fb..86b9359d 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/EmailVerificationQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/EmailVerificationQueries.java @@ -19,10 +19,8 @@ import io.supertokens.pluginInterface.RowMapper; 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.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; @@ -30,8 +28,7 @@ import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; +import java.util.*; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.update; @@ -52,7 +49,7 @@ static String getQueryToCreateEmailVerificationTable(Start start) { + " 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" + + " REFERENCES " + Config.getConfig(start).getAppsTable() + " (app_id) ON DELETE CASCADE" + ");"; // @formatter:on } @@ -71,13 +68,14 @@ static String getQueryToCreateEmailVerificationTokensTable(Start start) { + "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 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, 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" + + " REFERENCES " + Config.getConfig(start).getTenantsTable() + " (app_id, tenant_id) ON DELETE CASCADE" + ")"; // @formatter:on } @@ -100,7 +98,8 @@ public static void deleteExpiredEmailVerificationTokens(Start start) throws SQLE public static void updateUsersIsEmailVerified_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String userId, String email, - boolean isEmailVerified) throws SQLException, StorageQueryException { + boolean isEmailVerified) + throws SQLException, StorageQueryException { if (isEmailVerified) { String QUERY = "INSERT INTO " + getConfig(start).getEmailVerificationTable() @@ -124,8 +123,10 @@ public static void updateUsersIsEmailVerified_Transaction(Start start, Connectio } public static void deleteAllEmailVerificationTokensForUser_Transaction(Start start, Connection con, - TenantIdentifier tenantIdentifier, String userId, - String email) throws SQLException, StorageQueryException { + TenantIdentifier tenantIdentifier, + String userId, + String email) + throws SQLException, StorageQueryException { String QUERY = "DELETE FROM " + getConfig(start).getEmailVerificationTokensTable() + " WHERE app_id = ? AND tenant_id = ? AND user_id = ? AND email = ?"; @@ -137,7 +138,8 @@ public static void deleteAllEmailVerificationTokensForUser_Transaction(Start sta }); } - public static EmailVerificationTokenInfo getEmailVerificationTokenInfo(Start start, TenantIdentifier tenantIdentifier, + public static EmailVerificationTokenInfo getEmailVerificationTokenInfo(Start start, + TenantIdentifier tenantIdentifier, String token) throws SQLException, StorageQueryException { String QUERY = "SELECT user_id, token, token_expiry, email FROM " @@ -155,7 +157,8 @@ public static EmailVerificationTokenInfo getEmailVerificationTokenInfo(Start sta }); } - public static void addEmailVerificationToken(Start start, TenantIdentifier tenantIdentifier, 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, tenant_id, user_id, token, token_expiry, email)" + " VALUES(?, ?, ?, ?, ?, ?)"; @@ -173,10 +176,13 @@ public static void addEmailVerificationToken(Start start, TenantIdentifier tenan public static EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUser_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, - String userId, String email) throws SQLException, StorageQueryException { + String userId, + String email) + throws SQLException, StorageQueryException { String QUERY = "SELECT user_id, token, token_expiry, email FROM " - + getConfig(start).getEmailVerificationTokensTable() + " WHERE app_id = ? AND tenant_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, tenantIdentifier.getAppId()); @@ -199,9 +205,11 @@ public static EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUs public static EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUser(Start start, TenantIdentifier tenantIdentifier, String userId, - String email) throws SQLException, StorageQueryException { + String email) + throws SQLException, StorageQueryException { String QUERY = "SELECT user_id, token, token_expiry, email FROM " - + getConfig(start).getEmailVerificationTokensTable() + " WHERE app_id = ? AND tenant_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, tenantIdentifier.getAppId()); @@ -233,38 +241,130 @@ public static boolean isEmailVerified(Start start, AppIdentifier appIdentifier, }, result -> result.next()); } - 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 app_id = ? AND user_id = ?"; - update(sqlCon, QUERY, pst -> { - pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, userId); - }); - } + public static class UserIdAndEmail { + public String userId; + public String email; - { - String QUERY = "DELETE FROM " + getConfig(start).getEmailVerificationTokensTable() - + " WHERE app_id = ? AND user_id = ?"; + public UserIdAndEmail(String userId, String email) { + this.userId = userId; + this.email = email; + } + } + + // returns list of userIds where email is verified. + public static List isEmailVerified_transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, + List userIdAndEmail) + throws SQLException, StorageQueryException { + if (userIdAndEmail.isEmpty()) { + return new ArrayList<>(); + } + List emails = new ArrayList<>(); + List userIds = new ArrayList<>(); + Map userIdToEmailMap = new HashMap<>(); + for (UserIdAndEmail ue : userIdAndEmail) { + emails.add(ue.email); + userIds.add(ue.userId); + } + for (UserIdAndEmail ue : userIdAndEmail) { + if (userIdToEmailMap.containsKey(ue.userId)) { + throw new RuntimeException("Found a bug!"); + } + userIdToEmailMap.put(ue.userId, ue.email); + } + String QUERY = "SELECT * FROM " + getConfig(start).getEmailVerificationTable() + + " WHERE app_id = ? AND user_id IN (" + Utils.generateCommaSeperatedQuestionMarks(userIds.size()) + + ") AND email IN (" + Utils.generateCommaSeperatedQuestionMarks(emails.size()) + ")"; - update(sqlCon, QUERY, pst -> { - pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, userId); - }); + return execute(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + int index = 2; + for (String userId : userIds) { + pst.setString(index++, userId); + } + for (String email : emails) { + pst.setString(index++, email); + } + }, result -> { + List res = new ArrayList<>(); + while (result.next()) { + String userId = result.getString("user_id"); + String email = result.getString("email"); + if (Objects.equals(userIdToEmailMap.get(userId), email)) { + res.add(userId); } + } + return res; + }); + } - sqlCon.commit(); - } catch (SQLException throwables) { - throw new StorageTransactionLogicException(throwables); + public static List isEmailVerified(Start start, AppIdentifier appIdentifier, + List userIdAndEmail) + throws SQLException, StorageQueryException { + if (userIdAndEmail.isEmpty()) { + return new ArrayList<>(); + } + List emails = new ArrayList<>(); + List userIds = new ArrayList<>(); + Map userIdToEmailMap = new HashMap<>(); + for (UserIdAndEmail ue : userIdAndEmail) { + emails.add(ue.email); + userIds.add(ue.userId); + } + for (UserIdAndEmail ue : userIdAndEmail) { + if (userIdToEmailMap.containsKey(ue.userId)) { + throw new RuntimeException("Found a bug!"); } - return null; + userIdToEmailMap.put(ue.userId, ue.email); + } + String QUERY = "SELECT * FROM " + getConfig(start).getEmailVerificationTable() + + " WHERE app_id = ? AND user_id IN (" + Utils.generateCommaSeperatedQuestionMarks(userIds.size()) + + ") AND email IN (" + Utils.generateCommaSeperatedQuestionMarks(emails.size()) + ")"; + + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + int index = 2; + for (String userId : userIds) { + pst.setString(index++, userId); + } + for (String email : emails) { + pst.setString(index++, email); + } + }, result -> { + List res = new ArrayList<>(); + while (result.next()) { + String userId = result.getString("user_id"); + String email = result.getString("email"); + if (Objects.equals(userIdToEmailMap.get(userId), email)) { + res.add(userId); + } + } + return res; }); } + public static void deleteUserInfo_Transaction(Connection sqlCon, Start start, AppIdentifier appIdentifier, + String userId) + throws StorageQueryException, SQLException { + { + 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 app_id = ? AND user_id = ?"; + + update(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); + } + } + public static boolean deleteUserInfo(Start start, TenantIdentifier tenantIdentifier, String userId) throws StorageQueryException, SQLException { String QUERY = "DELETE FROM " + getConfig(start).getEmailVerificationTokensTable() 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 bc012896..81583518 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java @@ -20,6 +20,7 @@ import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.RowMapper; import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.authRecipe.LoginMethod; import io.supertokens.pluginInterface.dashboard.DashboardSearchTags; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; @@ -36,6 +37,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.*; +import java.util.stream.Collectors; import static io.supertokens.storage.postgresql.PreparedStatementValueSetter.NO_OP_SETTER; import static io.supertokens.storage.postgresql.ProcessState.PROCESS_STATE.CREATING_NEW_TABLE; @@ -72,13 +74,19 @@ static String getQueryToCreateUsersTable(Start start) { + "app_id VARCHAR(64) DEFAULT 'public'," + "tenant_id VARCHAR(64) DEFAULT 'public'," + "user_id CHAR(36) NOT NULL," + + "primary_or_recipe_user_id CHAR(36) NOT NULL," + + "is_linked_or_is_a_primary_user BOOLEAN NOT NULL DEFAULT FALSE," + "recipe_id VARCHAR(128) NOT NULL," + "time_joined BIGINT NOT NULL," + + "primary_or_recipe_user_time_joined BIGINT NOT NULL," + "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, "primary_or_recipe_user_id", "fkey") + + " FOREIGN KEY(app_id, primary_or_recipe_user_id)" + + " REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable() + " (app_id, user_id) ON DELETE CASCADE," + "CONSTRAINT " + Utils.getConstraintName(schema, usersTable, "user_id", "fkey") + " FOREIGN KEY(app_id, user_id)" + " REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable() + @@ -97,9 +105,44 @@ public static String getQueryToCreateTenantIdIndexForUsersTable(Start start) { + 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);"; + static String getQueryToCreateUserPaginationIndex1(Start start) { + return "CREATE INDEX all_auth_recipe_users_pagination_index1 ON " + Config.getConfig(start).getUsersTable() + + "(app_id, tenant_id, primary_or_recipe_user_time_joined DESC, primary_or_recipe_user_id DESC);"; + } + + static String getQueryToCreateUserPaginationIndex2(Start start) { + return "CREATE INDEX all_auth_recipe_users_pagination_index2 ON " + Config.getConfig(start).getUsersTable() + + "(app_id, tenant_id, primary_or_recipe_user_time_joined ASC, primary_or_recipe_user_id DESC);"; + } + + static String getQueryToCreateUserPaginationIndex3(Start start) { + return "CREATE INDEX all_auth_recipe_users_pagination_index3 ON " + Config.getConfig(start).getUsersTable() + + "(recipe_id, app_id, tenant_id, primary_or_recipe_user_time_joined DESC, primary_or_recipe_user_id DESC);"; + } + + static String getQueryToCreateUserPaginationIndex4(Start start) { + return "CREATE INDEX all_auth_recipe_users_pagination_index4 ON " + Config.getConfig(start).getUsersTable() + + "(recipe_id, app_id, tenant_id, primary_or_recipe_user_time_joined ASC, primary_or_recipe_user_id DESC);"; + } + + static String getQueryToCreatePrimaryUserId(Start start) { + /* + * Used in: + * - does user exist + * */ + return "CREATE INDEX all_auth_recipe_users_primary_user_id_index ON " + + Config.getConfig(start).getUsersTable() + + "(primary_or_recipe_user_id, app_id);"; + } + + static String getQueryToCreateRecipeIdIndex(Start start) { + /* + * Used in: + * - user count query + * */ + return "CREATE INDEX all_auth_recipe_users_recipe_id_index ON " + + Config.getConfig(start).getUsersTable() + + "(app_id, recipe_id, tenant_id);"; } private static String getQueryToCreateAppsTable(Start start) { @@ -109,8 +152,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 } @@ -169,8 +212,13 @@ private static String getQueryToCreateAppIdToUserIdTable(Start start) { + "app_id VARCHAR(64) NOT NULL DEFAULT 'public'," + "user_id CHAR(36) NOT NULL," + "recipe_id VARCHAR(128) NOT NULL," + + "primary_or_recipe_user_id CHAR(36) NOT NULL," + + "is_linked_or_is_a_primary_user BOOLEAN NOT NULL DEFAULT FALSE," + "CONSTRAINT " + Utils.getConstraintName(schema, appToUserTable, null, "pkey") + " PRIMARY KEY (app_id, user_id), " + + "CONSTRAINT " + Utils.getConstraintName(schema, appToUserTable, "primary_or_recipe_user_id", "fkey") + + " FOREIGN KEY(app_id, primary_or_recipe_user_id)" + + " REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable() + " (app_id, user_id) ON DELETE CASCADE," + "CONSTRAINT " + Utils.getConstraintName(schema, appToUserTable, "app_id", "fkey") + " FOREIGN KEY(app_id) REFERENCES " + Config.getConfig(start).getAppsTable() + " (app_id) ON DELETE CASCADE" @@ -223,7 +271,12 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto update(start, getQueryToCreateUsersTable(start), NO_OP_SETTER); // index - update(start, getQueryToCreateUserPaginationIndex(start), NO_OP_SETTER); + update(start, getQueryToCreateUserPaginationIndex1(start), NO_OP_SETTER); + update(start, getQueryToCreateUserPaginationIndex2(start), NO_OP_SETTER); + update(start, getQueryToCreateUserPaginationIndex3(start), NO_OP_SETTER); + update(start, getQueryToCreateUserPaginationIndex4(start), NO_OP_SETTER); + update(start, getQueryToCreatePrimaryUserId(start), NO_OP_SETTER); + update(start, getQueryToCreateRecipeIdIndex(start), NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getUserLastActiveTable())) { @@ -231,7 +284,8 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto update(start, ActiveUsersQueries.getQueryToCreateUserLastActiveTable(start), NO_OP_SETTER); // Index - update(start, ActiveUsersQueries.getQueryToCreateAppIdIndexForUserLastActiveTable(start), NO_OP_SETTER); + update(start, ActiveUsersQueries.getQueryToCreateAppIdIndexForUserLastActiveTable(start), + NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getAccessTokenSigningKeysTable())) { @@ -262,7 +316,8 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto NO_OP_SETTER); // index - update(start, MultitenancyQueries.getQueryToCreateTenantIdIndexForTenantThirdPartyProvidersTable(start), + update(start, + MultitenancyQueries.getQueryToCreateTenantIdIndexForTenantThirdPartyProvidersTable(start), NO_OP_SETTER); } @@ -272,7 +327,9 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto NO_OP_SETTER); // index - update(start, MultitenancyQueries.getQueryToCreateThirdPartyIdIndexForTenantThirdPartyProviderClientsTable(start), + update(start, + MultitenancyQueries.getQueryToCreateThirdPartyIdIndexForTenantThirdPartyProviderClientsTable( + start), NO_OP_SETTER); } @@ -391,7 +448,8 @@ 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); + update(start, UserRolesQueries.getQueryToCreateRoleIndexForRolePermissionsTable(start), + NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getUserRolesTable())) { @@ -409,7 +467,9 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto update(start, UserIdMappingQueries.getQueryToCreateUserIdMappingTable(start), NO_OP_SETTER); // index - update(start, UserIdMappingQueries.getQueryToCreateSupertokensUserIdIndexForUserIdMappingTable(start), NO_OP_SETTER); + update(start, + UserIdMappingQueries.getQueryToCreateSupertokensUserIdIndexForUserIdMappingTable(start), + NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getDashboardUsersTable())) { @@ -417,7 +477,8 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto update(start, DashboardQueries.getQueryToCreateDashboardUsersTable(start), NO_OP_SETTER); // Index - update(start, DashboardQueries.getQueryToCreateAppIdIndexForDashboardUsersTable(start), NO_OP_SETTER); + update(start, DashboardQueries.getQueryToCreateAppIdIndexForDashboardUsersTable(start), + NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getDashboardSessionsTable())) { @@ -604,7 +665,9 @@ public static void deleteKeyValue_Transaction(Start start, Connection con, Tenan 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()); + StringBuilder QUERY = new StringBuilder( + "SELECT COUNT(DISTINCT primary_or_recipe_user_id) AS total FROM " + + getConfig(start).getUsersTable()); QUERY.append(" WHERE app_id = ?"); if (includeRecipeIds != null && includeRecipeIds.length > 0) { QUERY.append(" AND recipe_id IN ("); @@ -636,7 +699,8 @@ public static long getUsersCount(Start start, AppIdentifier appIdentifier, RECIP 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()); + StringBuilder QUERY = new StringBuilder( + "SELECT COUNT(DISTINCT primary_or_recipe_user_id) 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 ("); @@ -669,7 +733,8 @@ public static long getUsersCount(Start start, TenantIdentifier tenantIdentifier, public static boolean doesUserIdExist(Start start, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { - + // We query both tables cause there is a case where a primary user ID exists, but its associated + // recipe user ID has been deleted AND there are other recipe user IDs linked to this primary user ID already. String QUERY = "SELECT 1 FROM " + getConfig(start).getAppIdToUserIdTable() + " WHERE app_id = ? AND user_id = ?"; return execute(start, QUERY, pst -> { @@ -678,15 +743,33 @@ public static boolean doesUserIdExist(Start start, AppIdentifier appIdentifier, }, ResultSet::next); } - public static boolean doesUserIdExist(Start start, TenantIdentifier tenantIdentifier, String userId) + public static boolean doesUserIdExist_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { + // We query both tables cause there is a case where a primary user ID exists, but its associated + // recipe user ID has been deleted AND there are other recipe user IDs linked to this primary user ID already. + String QUERY = "SELECT 1 FROM " + getConfig(start).getAppIdToUserIdTable() + + " WHERE app_id = ? AND user_id = ?"; + return execute(sqlCon, 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 { + // We query both tables cause there is a case where a primary user ID exists, but its associated + // recipe user ID has been deleted AND there are other recipe user IDs linked to this primary user ID already. String QUERY = "SELECT 1 FROM " + getConfig(start).getUsersTable() - + " WHERE app_id = ? AND tenant_id = ? AND user_id = ?"; + + " WHERE app_id = ? AND tenant_id = ? AND user_id = ? UNION SELECT 1 FROM " + + getConfig(start).getUsersTable() + + " WHERE app_id = ? AND tenant_id = ? AND primary_or_recipe_user_id = ?"; return execute(start, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, tenantIdentifier.getTenantId()); pst.setString(3, userId); + pst.setString(4, tenantIdentifier.getAppId()); + pst.setString(5, tenantIdentifier.getTenantId()); + pst.setString(6, userId); }, ResultSet::next); } @@ -698,7 +781,7 @@ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenant throws SQLException, StorageQueryException { // This list will be used to keep track of the result's order from the db - List usersFromQuery; + List usersFromQuery; if (dashboardSearchTags != null) { ArrayList queryList = new ArrayList<>(); @@ -740,7 +823,9 @@ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenant String QUERY = "SELECT allAuthUsersTable.*" + " FROM " + getConfig(start).getUsersTable() + " AS allAuthUsersTable" + " JOIN " + getConfig(start).getThirdPartyUserToTenantTable() - + " AS thirdPartyToTenantTable ON allAuthUsersTable.app_id = thirdPartyToTenantTable.app_id AND" + + + " AS thirdPartyToTenantTable ON allAuthUsersTable.app_id = thirdPartyToTenantTable" + + ".app_id AND" + " allAuthUsersTable.tenant_id = thirdPartyToTenantTable.tenant_id AND" + " allAuthUsersTable.user_id = thirdPartyToTenantTable.user_id" + " JOIN " + getConfig(start).getThirdPartyUsersTable() @@ -870,22 +955,20 @@ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenant usersFromQuery = new ArrayList<>(); } else { - String finalQuery = "SELECT * FROM ( " + USER_SEARCH_TAG_CONDITION.toString() + " )" - + " AS finalResultTable ORDER BY time_joined " + timeJoinedOrder + ", user_id DESC "; + String finalQuery = "SELECT DISTINCT primary_or_recipe_user_id, primary_or_recipe_user_time_joined FROM ( " + USER_SEARCH_TAG_CONDITION.toString() + " )" + + " AS finalResultTable ORDER BY primary_or_recipe_user_time_joined " + timeJoinedOrder + ", primary_or_recipe_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<>(); + List temp = new ArrayList<>(); while (result.next()) { - temp.add(new UserInfoPaginationResultHolder(result.getString("user_id"), - result.getString("recipe_id"))); + temp.add(result.getString("primary_or_recipe_user_id")); } return temp; }); } - } } else { @@ -909,11 +992,11 @@ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenant recipeIdCondition = recipeIdCondition + " AND"; } 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 <= ?)) AND app_id = ? AND tenant_id = ?" - + " ORDER BY time_joined " + timeJoinedOrder - + ", user_id DESC LIMIT ?"; + String QUERY = "SELECT DISTINCT primary_or_recipe_user_id, primary_or_recipe_user_time_joined FROM " + getConfig(start).getUsersTable() + " WHERE " + + recipeIdCondition + " (primary_or_recipe_user_time_joined " + timeJoinedOrderSymbol + + " ? OR (primary_or_recipe_user_time_joined = ? AND primary_or_recipe_user_id <= ?)) AND app_id = ? AND tenant_id = ?" + + " ORDER BY primary_or_recipe_user_time_joined " + timeJoinedOrder + + ", primary_or_recipe_user_id DESC LIMIT ?"; usersFromQuery = execute(start, QUERY, pst -> { if (includeRecipeIds != null) { for (int i = 0; i < includeRecipeIds.length; i++) { @@ -929,21 +1012,20 @@ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenant pst.setString(baseIndex + 5, tenantIdentifier.getTenantId()); pst.setInt(baseIndex + 6, limit); }, result -> { - List temp = new ArrayList<>(); + List temp = new ArrayList<>(); while (result.next()) { - temp.add(new UserInfoPaginationResultHolder(result.getString("user_id"), - result.getString("recipe_id"))); + temp.add(result.getString("primary_or_recipe_user_id")); } return temp; }); } else { String recipeIdCondition = RECIPE_ID_CONDITION.toString(); - String QUERY = "SELECT user_id, recipe_id FROM " + getConfig(start).getUsersTable() + " WHERE "; + String QUERY = "SELECT DISTINCT primary_or_recipe_user_id, primary_or_recipe_user_time_joined FROM " + getConfig(start).getUsersTable() + " WHERE "; if (!recipeIdCondition.equals("")) { QUERY += recipeIdCondition + " AND"; } - QUERY += " app_id = ? AND tenant_id = ? ORDER BY time_joined " + timeJoinedOrder - + ", user_id DESC LIMIT ?"; + QUERY += " app_id = ? AND tenant_id = ? ORDER BY primary_or_recipe_user_time_joined " + timeJoinedOrder + + ", primary_or_recipe_user_id DESC LIMIT ?"; usersFromQuery = execute(start, QUERY, pst -> { if (includeRecipeIds != null) { for (int i = 0; i < includeRecipeIds.length; i++) { @@ -956,75 +1038,507 @@ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenant pst.setString(baseIndex + 2, tenantIdentifier.getTenantId()); pst.setInt(baseIndex + 3, limit); }, result -> { - List temp = new ArrayList<>(); + List temp = new ArrayList<>(); while (result.next()) { - temp.add(new UserInfoPaginationResultHolder(result.getString("user_id"), - result.getString("recipe_id"))); + temp.add(result.getString("primary_or_recipe_user_id")); } return temp; }); } } - // we create a map from recipe ID -> userId[] - Map> recipeIdToUserIdListMap = new HashMap<>(); - for (UserInfoPaginationResultHolder user : usersFromQuery) { - RECIPE_ID recipeId = RECIPE_ID.getEnumFromString(user.recipeId); - if (recipeId == null) { - throw new SQLException("Unrecognised recipe ID in database: " + user.recipeId); + AuthRecipeUserInfo[] finalResult = new AuthRecipeUserInfo[usersFromQuery.size()]; + + List users = getPrimaryUserInfoForUserIds(start, + tenantIdentifier.toAppIdentifier(), + usersFromQuery); + + // we fill in all the slots in finalResult based on their position in + // usersFromQuery + Map userIdToInfoMap = new HashMap<>(); + for (AuthRecipeUserInfo user : users) { + userIdToInfoMap.put(user.getSupertokensUserId(), user); + } + for (int i = 0; i < usersFromQuery.size(); i++) { + if (finalResult[i] == null) { + finalResult[i] = userIdToInfoMap.get(usersFromQuery.get(i)); + } + } + + return finalResult; + } + + public static void makePrimaryUser_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, + String userId) + throws SQLException, StorageQueryException { + { + String QUERY = "UPDATE " + getConfig(start).getUsersTable() + + " SET is_linked_or_is_a_primary_user = true WHERE app_id = ? AND user_id = ?"; + + update(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); + } + { + String QUERY = "UPDATE " + getConfig(start).getAppIdToUserIdTable() + + " SET is_linked_or_is_a_primary_user = true WHERE app_id = ? AND user_id = ?"; + + update(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); + } + } + + public static void linkAccounts_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, + String recipeUserId, String primaryUserId) + throws SQLException, StorageQueryException { + { + String QUERY = "UPDATE " + getConfig(start).getUsersTable() + + " SET is_linked_or_is_a_primary_user = true, primary_or_recipe_user_id = ? WHERE app_id = ? AND " + + "user_id = ?"; + + update(sqlCon, QUERY, pst -> { + pst.setString(1, primaryUserId); + pst.setString(2, appIdentifier.getAppId()); + pst.setString(3, recipeUserId); + }); + } + + updateTimeJoinedForPrimaryUser_Transaction(start, sqlCon, appIdentifier, primaryUserId); + + { + String QUERY = "UPDATE " + getConfig(start).getAppIdToUserIdTable() + + " SET is_linked_or_is_a_primary_user = true, primary_or_recipe_user_id = ? WHERE app_id = ? AND " + + "user_id = ?"; + + update(sqlCon, QUERY, pst -> { + pst.setString(1, primaryUserId); + pst.setString(2, appIdentifier.getAppId()); + pst.setString(3, recipeUserId); + }); + } + } + + public static void unlinkAccounts_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, + String primaryUserId, String recipeUserId) + throws SQLException, StorageQueryException { + { + String QUERY = "UPDATE " + getConfig(start).getUsersTable() + + " SET is_linked_or_is_a_primary_user = false, primary_or_recipe_user_id = ?, " + + "primary_or_recipe_user_time_joined = time_joined WHERE app_id = ? AND " + + "user_id = ?"; + + update(sqlCon, QUERY, pst -> { + pst.setString(1, recipeUserId); + pst.setString(2, appIdentifier.getAppId()); + pst.setString(3, recipeUserId); + }); + } + + updateTimeJoinedForPrimaryUser_Transaction(start, sqlCon, appIdentifier, primaryUserId); + + { + String QUERY = "UPDATE " + getConfig(start).getAppIdToUserIdTable() + + " SET is_linked_or_is_a_primary_user = false, primary_or_recipe_user_id = ?" + + " WHERE app_id = ? AND user_id = ?"; + + update(sqlCon, QUERY, pst -> { + pst.setString(1, recipeUserId); + pst.setString(2, appIdentifier.getAppId()); + pst.setString(3, recipeUserId); + }); + } + } + + public static AuthRecipeUserInfo[] listPrimaryUsersByPhoneNumber_Transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + String phoneNumber) + throws SQLException, StorageQueryException { + // we first lock on the table based on phoneNumber and tenant - this will ensure that any other + // query happening related to the account linking on this phone number / tenant will wait for this to finish, + // and vice versa. + + PasswordlessQueries.lockPhoneAndTenant_Transaction(start, sqlCon, appIdentifier, phoneNumber); + + // now that we have locks on all the relevant tables, we can read from them safely + List userIds = PasswordlessQueries.listUserIdsByPhoneNumber_Transaction(start, sqlCon, appIdentifier, + phoneNumber); + + List result = getPrimaryUserInfoForUserIds_Transaction(start, sqlCon, appIdentifier, + userIds); + + // this is going to order them based on oldest that joined to newest that joined. + result.sort(Comparator.comparingLong(o -> o.timeJoined)); + + return result.toArray(new AuthRecipeUserInfo[0]); + } + + public static AuthRecipeUserInfo[] listPrimaryUsersByThirdPartyInfo(Start start, + AppIdentifier appIdentifier, + String thirdPartyId, + String thirdPartyUserId) + throws SQLException, StorageQueryException { + List userIds = ThirdPartyQueries.listUserIdsByThirdPartyInfo(start, appIdentifier, + thirdPartyId, thirdPartyUserId); + List result = getPrimaryUserInfoForUserIds(start, appIdentifier, userIds); + + // this is going to order them based on oldest that joined to newest that joined. + result.sort(Comparator.comparingLong(o -> o.timeJoined)); + + return result.toArray(new AuthRecipeUserInfo[0]); + } + + public static AuthRecipeUserInfo[] listPrimaryUsersByThirdPartyInfo_Transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + String thirdPartyId, + String thirdPartyUserId) + throws SQLException, StorageQueryException { + // we first lock on the table based on thirdparty info and tenant - this will ensure that any other + // query happening related to the account linking on this third party info / tenant will wait for this to + // finish, + // and vice versa. + + ThirdPartyQueries.lockThirdPartyInfoAndTenant_Transaction(start, sqlCon, appIdentifier, thirdPartyId, + thirdPartyUserId); + + // now that we have locks on all the relevant tables, we can read from them safely + List userIds = ThirdPartyQueries.listUserIdsByThirdPartyInfo_Transaction(start, sqlCon, appIdentifier, + thirdPartyId, thirdPartyUserId); + List result = getPrimaryUserInfoForUserIds_Transaction(start, sqlCon, appIdentifier, userIds); + + // this is going to order them based on oldest that joined to newest that joined. + result.sort(Comparator.comparingLong(o -> o.timeJoined)); + + return result.toArray(new AuthRecipeUserInfo[0]); + } + + public static AuthRecipeUserInfo[] listPrimaryUsersByEmail_Transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + String email) + throws SQLException, StorageQueryException { + // we first lock on the three tables based on email and tenant - this will ensure that any other + // query happening related to the account linking on this email / tenant will wait for this to finish, + // and vice versa. + + EmailPasswordQueries.lockEmail_Transaction(start, sqlCon, appIdentifier, email); + + ThirdPartyQueries.lockEmail_Transaction(start, sqlCon, appIdentifier, email); + + PasswordlessQueries.lockEmail_Transaction(start, sqlCon, appIdentifier, email); + + // now that we have locks on all the relevant tables, we can read from them safely + List userIds = new ArrayList<>(); + userIds.addAll(EmailPasswordQueries.getPrimaryUserIdsUsingEmail_Transaction(start, sqlCon, appIdentifier, + email)); + + userIds.addAll(PasswordlessQueries.getPrimaryUserIdsUsingEmail_Transaction(start, sqlCon, appIdentifier, + email)); + + userIds.addAll(ThirdPartyQueries.getPrimaryUserIdUsingEmail_Transaction(start, sqlCon, appIdentifier, email)); + + // remove duplicates from userIds + Set userIdsSet = new HashSet<>(userIds); + userIds = new ArrayList<>(userIdsSet); + + List result = getPrimaryUserInfoForUserIds(start, appIdentifier, + userIds); + + // this is going to order them based on oldest that joined to newest that joined. + result.sort(Comparator.comparingLong(o -> o.timeJoined)); + + return result.toArray(new AuthRecipeUserInfo[0]); + } + + public static AuthRecipeUserInfo[] listPrimaryUsersByEmail(Start start, TenantIdentifier tenantIdentifier, + String email) + throws StorageQueryException, SQLException { + List userIds = new ArrayList<>(); + String emailPasswordUserId = EmailPasswordQueries.getPrimaryUserIdUsingEmail(start, tenantIdentifier, + email); + if (emailPasswordUserId != null) { + userIds.add(emailPasswordUserId); + } + + String passwordlessUserId = PasswordlessQueries.getPrimaryUserIdUsingEmail(start, tenantIdentifier, + email); + if (passwordlessUserId != null) { + userIds.add(passwordlessUserId); + } + + userIds.addAll(ThirdPartyQueries.getPrimaryUserIdUsingEmail(start, tenantIdentifier, email)); + + // remove duplicates from userIds + Set userIdsSet = new HashSet<>(userIds); + userIds = new ArrayList<>(userIdsSet); + + List result = getPrimaryUserInfoForUserIds(start, tenantIdentifier.toAppIdentifier(), + userIds); + + // this is going to order them based on oldest that joined to newest that joined. + result.sort(Comparator.comparingLong(o -> o.timeJoined)); + + return result.toArray(new AuthRecipeUserInfo[0]); + } + + public static AuthRecipeUserInfo[] listPrimaryUsersByPhoneNumber(Start start, + TenantIdentifier tenantIdentifier, + String phoneNumber) + throws StorageQueryException, SQLException { + List userIds = new ArrayList<>(); + + String passwordlessUserId = PasswordlessQueries.getPrimaryUserByPhoneNumber(start, tenantIdentifier, + phoneNumber); + if (passwordlessUserId != null) { + userIds.add(passwordlessUserId); + } + + List result = getPrimaryUserInfoForUserIds(start, tenantIdentifier.toAppIdentifier(), + userIds); + + // this is going to order them based on oldest that joined to newest that joined. + result.sort(Comparator.comparingLong(o -> o.timeJoined)); + + return result.toArray(new AuthRecipeUserInfo[0]); + } + + public static AuthRecipeUserInfo getPrimaryUserByThirdPartyInfo(Start start, + TenantIdentifier tenantIdentifier, + String thirdPartyId, + String thirdPartyUserId) + throws StorageQueryException, SQLException { + String userId = ThirdPartyQueries.getUserIdByThirdPartyInfo(start, tenantIdentifier, + thirdPartyId, thirdPartyUserId); + return getPrimaryUserInfoForUserId(start, tenantIdentifier.toAppIdentifier(), userId); + } + + public static String getPrimaryUserIdStrForUserId(Start start, AppIdentifier appIdentifier, String id) + throws SQLException, StorageQueryException { + String QUERY = "SELECT primary_or_recipe_user_id FROM " + getConfig(start).getUsersTable() + + " WHERE user_id = ? AND app_id = ?"; + return execute(start, QUERY, pst -> { + pst.setString(1, id); + pst.setString(2, appIdentifier.getAppId()); + }, result -> { + if (result.next()) { + return result.getString("primary_or_recipe_user_id"); + } + return null; + }); + } + + public static AuthRecipeUserInfo getPrimaryUserInfoForUserId(Start start, AppIdentifier appIdentifier, String id) + throws SQLException, StorageQueryException { + List ids = new ArrayList<>(); + ids.add(id); + List result = getPrimaryUserInfoForUserIds(start, appIdentifier, ids); + if (result.isEmpty()) { + return null; + } + return result.get(0); + } + + public static AuthRecipeUserInfo getPrimaryUserInfoForUserId_Transaction(Start start, Connection con, + AppIdentifier appIdentifier, String id) + throws SQLException, StorageQueryException { + List ids = new ArrayList<>(); + ids.add(id); + List result = getPrimaryUserInfoForUserIds_Transaction(start, con, appIdentifier, ids); + if (result.isEmpty()) { + return null; + } + return result.get(0); + } + + private static List getPrimaryUserInfoForUserIds(Start start, + AppIdentifier appIdentifier, + List userIds) + throws StorageQueryException, SQLException { + if (userIds.size() == 0) { + return new ArrayList<>(); + } + + // We check both user_id and primary_or_recipe_user_id because the input may have a recipe userId + // which is linked to a primary user ID in which case it won't be in the primary_or_recipe_user_id column, + // or the input may have a primary user ID whose recipe user ID was removed, so it won't be in the user_id + // column + String QUERY = "SELECT au.user_id, au.primary_or_recipe_user_id, au.is_linked_or_is_a_primary_user, au.recipe_id, aaru.tenant_id, aaru.time_joined FROM " + getConfig(start).getAppIdToUserIdTable() + " as au " + + "LEFT JOIN " + getConfig(start).getUsersTable() + " as aaru ON au.app_id = aaru.app_id AND au.user_id = aaru.user_id" + + " WHERE au.primary_or_recipe_user_id IN (SELECT primary_or_recipe_user_id FROM " + + getConfig(start).getAppIdToUserIdTable() + " WHERE (user_id IN (" + + Utils.generateCommaSeperatedQuestionMarks(userIds.size()) + + ") OR au.primary_or_recipe_user_id IN (" + + Utils.generateCommaSeperatedQuestionMarks(userIds.size()) + + ")) AND app_id = ?) AND au.app_id = ?"; + + List allAuthUsersResult = execute(start, QUERY, pst -> { + // IN user_id + int index = 1; + for (int i = 0; i < userIds.size(); i++, index++) { + pst.setString(index, userIds.get(i)); } - List userIdList = recipeIdToUserIdListMap.get(recipeId); - if (userIdList == null) { - userIdList = new ArrayList<>(); + // IN primary_or_recipe_user_id + for (int i = 0; i < userIds.size(); i++, index++) { + pst.setString(index, userIds.get(i)); } - userIdList.add(user.userId); - recipeIdToUserIdListMap.put(recipeId, userIdList); + // for app_id + pst.setString(index, appIdentifier.getAppId()); + pst.setString(index + 1, appIdentifier.getAppId()); + }, result -> { + List parsedResult = new ArrayList<>(); + while (result.next()) { + parsedResult.add(new AllAuthRecipeUsersResultHolder(result.getString("user_id"), + result.getString("tenant_id"), + result.getString("primary_or_recipe_user_id"), + result.getBoolean("is_linked_or_is_a_primary_user"), + result.getString("recipe_id"), + result.getLong("time_joined"))); + } + return parsedResult; + }); + + // Now we form the userIds again, but based on the user_id in the result from above. + Set recipeUserIdsToFetch = new HashSet<>(); + for (AllAuthRecipeUsersResultHolder user : allAuthUsersResult) { + // this will remove duplicate entries wherein a user id is shared across several tenants. + recipeUserIdsToFetch.add(user.userId); } - AuthRecipeUserInfo[] finalResult = new AuthRecipeUserInfo[usersFromQuery.size()]; + List loginMethods = new ArrayList<>(); + loginMethods.addAll( + EmailPasswordQueries.getUsersInfoUsingIdList(start, recipeUserIdsToFetch, appIdentifier)); + loginMethods.addAll(ThirdPartyQueries.getUsersInfoUsingIdList(start, recipeUserIdsToFetch, appIdentifier)); + loginMethods.addAll( + PasswordlessQueries.getUsersInfoUsingIdList(start, recipeUserIdsToFetch, appIdentifier)); + + Map recipeUserIdToLoginMethodMap = new HashMap<>(); + for (LoginMethod loginMethod : loginMethods) { + recipeUserIdToLoginMethodMap.put(loginMethod.getSupertokensUserId(), loginMethod); + } - // we give the userId[] for each recipe to fetch all those user's details - for (RECIPE_ID recipeId : recipeIdToUserIdListMap.keySet()) { - List users = getUserInfoForRecipeIdFromUserIds(start, - tenantIdentifier.toAppIdentifier(), recipeId, recipeIdToUserIdListMap.get(recipeId)); + Map userIdToAuthRecipeUserInfo = new HashMap<>(); - // we fill in all the slots in finalResult based on their position in - // usersFromQuery - Map userIdToInfoMap = new HashMap<>(); - for (AuthRecipeUserInfo user : users) { - userIdToInfoMap.put(user.id, user); + for (AllAuthRecipeUsersResultHolder authRecipeUsersResultHolder : allAuthUsersResult) { + String recipeUserId = authRecipeUsersResultHolder.userId; + LoginMethod loginMethod = recipeUserIdToLoginMethodMap.get(recipeUserId); + + if (loginMethod == null) { + // loginMethod will be null for primaryUserId for which the user has been deleted during unlink + continue; } - for (int i = 0; i < usersFromQuery.size(); i++) { - if (finalResult[i] == null) { - finalResult[i] = userIdToInfoMap.get(usersFromQuery.get(i).userId); - } + + String primaryUserId = authRecipeUsersResultHolder.primaryOrRecipeUserId; + AuthRecipeUserInfo curr = userIdToAuthRecipeUserInfo.get(primaryUserId); + if (curr == null) { + curr = AuthRecipeUserInfo.create(primaryUserId, authRecipeUsersResultHolder.isLinkedOrIsAPrimaryUser, + loginMethod); + } else { + curr.addLoginMethod(loginMethod); } + userIdToAuthRecipeUserInfo.put(primaryUserId, curr); } - return finalResult; + return userIdToAuthRecipeUserInfo.keySet().stream().map(userIdToAuthRecipeUserInfo::get) + .collect(Collectors.toList()); } - private static List getUserInfoForRecipeIdFromUserIds(Start start, - AppIdentifier appIdentifier, - RECIPE_ID recipeId, - List userIds) + private static List getPrimaryUserInfoForUserIds_Transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + List userIds) throws StorageQueryException, SQLException { - if (recipeId == RECIPE_ID.EMAIL_PASSWORD) { - return EmailPasswordQueries.getUsersInfoUsingIdList(start, appIdentifier, userIds); - } else if (recipeId == RECIPE_ID.THIRD_PARTY) { - return ThirdPartyQueries.getUsersInfoUsingIdList(start, appIdentifier, userIds); - } else if (recipeId == RECIPE_ID.PASSWORDLESS) { - return PasswordlessQueries.getUsersByIdList(start, appIdentifier, userIds); - } else { - throw new IllegalArgumentException("No implementation of get users for recipe: " + recipeId.toString()); + if (userIds.size() == 0) { + return new ArrayList<>(); + } + + // We check both user_id and primary_or_recipe_user_id because the input may have a recipe userId + // which is linked to a primary user ID in which case it won't be in the primary_or_recipe_user_id column, + // or the input may have a primary user ID whose recipe user ID was removed, so it won't be in the user_id + // column + String QUERY = "SELECT au.user_id, au.primary_or_recipe_user_id, au.is_linked_or_is_a_primary_user, au.recipe_id, aaru.tenant_id, aaru.time_joined FROM " + getConfig(start).getAppIdToUserIdTable() + " as au" + + " LEFT JOIN " + getConfig(start).getUsersTable() + " as aaru ON au.app_id = aaru.app_id AND au.user_id = aaru.user_id" + + " WHERE au.primary_or_recipe_user_id IN (SELECT primary_or_recipe_user_id FROM " + + getConfig(start).getAppIdToUserIdTable() + " WHERE (user_id IN (" + + Utils.generateCommaSeperatedQuestionMarks(userIds.size()) + + ") OR au.primary_or_recipe_user_id IN (" + + Utils.generateCommaSeperatedQuestionMarks(userIds.size()) + + ")) AND app_id = ?) AND au.app_id = ?"; + + List allAuthUsersResult = execute(sqlCon, QUERY, pst -> { + // IN user_id + int index = 1; + for (int i = 0; i < userIds.size(); i++, index++) { + pst.setString(index, userIds.get(i)); + } + // IN primary_or_recipe_user_id + for (int i = 0; i < userIds.size(); i++, index++) { + pst.setString(index, userIds.get(i)); + } + // for app_id + pst.setString(index, appIdentifier.getAppId()); + pst.setString(index + 1, appIdentifier.getAppId()); + }, result -> { + List parsedResult = new ArrayList<>(); + while (result.next()) { + parsedResult.add(new AllAuthRecipeUsersResultHolder(result.getString("user_id"), + result.getString("tenant_id"), + result.getString("primary_or_recipe_user_id"), + result.getBoolean("is_linked_or_is_a_primary_user"), + result.getString("recipe_id"), + result.getLong("time_joined"))); + } + return parsedResult; + }); + + // Now we form the userIds again, but based on the user_id in the result from above. + Set recipeUserIdsToFetch = new HashSet<>(); + for (AllAuthRecipeUsersResultHolder user : allAuthUsersResult) { + // this will remove duplicate entries wherein a user id is shared across several tenants. + recipeUserIdsToFetch.add(user.userId); + } + + List loginMethods = new ArrayList<>(); + loginMethods.addAll( + EmailPasswordQueries.getUsersInfoUsingIdList_Transaction(start, sqlCon, recipeUserIdsToFetch, appIdentifier)); + loginMethods.addAll(ThirdPartyQueries.getUsersInfoUsingIdList_Transaction(start, sqlCon, recipeUserIdsToFetch, appIdentifier)); + loginMethods.addAll( + PasswordlessQueries.getUsersInfoUsingIdList_Transaction(start, sqlCon, recipeUserIdsToFetch, appIdentifier)); + + Map recipeUserIdToLoginMethodMap = new HashMap<>(); + for (LoginMethod loginMethod : loginMethods) { + recipeUserIdToLoginMethodMap.put(loginMethod.getSupertokensUserId(), loginMethod); + } + + Map userIdToAuthRecipeUserInfo = new HashMap<>(); + + for (AllAuthRecipeUsersResultHolder authRecipeUsersResultHolder : allAuthUsersResult) { + String recipeUserId = authRecipeUsersResultHolder.userId; + LoginMethod loginMethod = recipeUserIdToLoginMethodMap.get(recipeUserId); + if (loginMethod == null) { + // loginMethod will be null for primaryUserId for which the user has been deleted during unlink + continue; + } + String primaryUserId = authRecipeUsersResultHolder.primaryOrRecipeUserId; + AuthRecipeUserInfo curr = userIdToAuthRecipeUserInfo.get(primaryUserId); + if (curr == null) { + curr = AuthRecipeUserInfo.create(primaryUserId, authRecipeUsersResultHolder.isLinkedOrIsAPrimaryUser, + loginMethod); + } else { + curr.addLoginMethod(loginMethod); + } + userIdToAuthRecipeUserInfo.put(primaryUserId, curr); } + + return userIdToAuthRecipeUserInfo.keySet().stream().map(userIdToAuthRecipeUserInfo::get) + .collect(Collectors.toList()); } - public static String getRecipeIdForUser_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId) + 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); @@ -1036,12 +1550,14 @@ public static String getRecipeIdForUser_Transaction(Start start, Connection sqlC }); } - public static Map> getTenantIdsForUserIds_transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, String[] userIds) + public static Map> getTenantIdsForUserIds_transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + 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 app_id = ? AND user_id IN ("); + QUERY.append(" WHERE user_id IN ("); for (int i = 0; i < userIds.length; i++) { QUERY.append("?"); @@ -1050,14 +1566,57 @@ public static Map> getTenantIdsForUserIds_transaction(Start QUERY.append(","); } } - QUERY.append(")"); + QUERY.append(") AND app_id = ?"); return execute(sqlCon, QUERY.toString(), pst -> { - pst.setString(1, appIdentifier.getAppId()); for (int i = 0; i < userIds.length; i++) { - // i+2 cause this starts with 1 and not 0, and 1 is appId - pst.setString(i + 2, userIds[i]); + // i+1 cause this starts with 1 and not 0, and 1 is appId + pst.setString(i + 1, userIds[i]); + } + pst.setString(userIds.length + 1, appIdentifier.getAppId()); + }, result -> { + Map> finalResult = new HashMap<>(); + for (String userId : userIds) { + finalResult.put(userId, new ArrayList<>()); + } + + while (result.next()) { + String userId = result.getString("user_id").trim(); + String tenantId = result.getString("tenant_id"); + + finalResult.get(userId).add(tenantId); + } + return finalResult; + }); + } + + return new HashMap<>(); + } + + public static Map> getTenantIdsForUserIds(Start start, + AppIdentifier appIdentifier, + 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(") AND app_id = ?"); + + return execute(start, QUERY.toString(), pst -> { + for (int i = 0; i < userIds.length; i++) { + // i+1 cause this starts with 1 and not 0, and 1 is appId + pst.setString(i + 1, userIds[i]); + } + pst.setString(userIds.length + 1, appIdentifier.getAppId()); }, result -> { Map> finalResult = new HashMap<>(); for (String userId : userIds) { @@ -1107,12 +1666,13 @@ public static String[] getAllTablesInTheDatabaseThatHasDataForAppId(Start start, List result = new ArrayList<>(); for (String tableName : tableNames) { - String QUERY = "SELECT 1 FROM " + Config.getConfig(start).getTableSchema() + "." + tableName + " WHERE app_id = ?"; + String QUERY = + "SELECT 1 FROM " + Config.getConfig(start).getTableSchema() + "." + tableName + " WHERE app_id = ?"; boolean hasRows = execute(start, QUERY, pst -> { pst.setString(1, appId); }, res -> { - return res.next(); + return res.next(); }); if (hasRows) { result.add(tableName); @@ -1122,13 +1682,87 @@ public static String[] getAllTablesInTheDatabaseThatHasDataForAppId(Start start, return result.toArray(new String[0]); } - private static class UserInfoPaginationResultHolder { - String userId; - String recipeId; + public static int getUsersCountWithMoreThanOneLoginMethod(Start start, AppIdentifier appIdentifier) + throws SQLException, StorageQueryException { + String QUERY = "SELECT COUNT (1) as c FROM (" + + " SELECT COUNT(user_id) as num_login_methods " + + " FROM " + getConfig(start).getUsersTable() + + " WHERE app_id = ? " + + " GROUP BY (app_id, primary_or_recipe_user_id) " + + ") as nloginmethods WHERE num_login_methods > 1"; + + return execute(start, QUERY, pst -> { + pst.setString(1, 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 " + + getConfig(start).getUsersTable() + + " WHERE app_id = ? AND is_linked_or_is_a_primary_user = true LIMIT 1"; + + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + }, result -> { + return result.next(); + }); + } + + public static AccountLinkingInfo getAccountLinkingInfo_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, String userId) + throws SQLException, StorageQueryException { + GeneralQueries.AccountLinkingInfo accountLinkingInfo = new GeneralQueries.AccountLinkingInfo(userId, false); + { + String QUERY = "SELECT primary_or_recipe_user_id, is_linked_or_is_a_primary_user FROM " + + Config.getConfig(start).getAppIdToUserIdTable() + " WHERE app_id = ? AND user_id = ?"; + + accountLinkingInfo = execute(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }, result -> { + if (result.next()) { + String primaryUserId1 = result.getString("primary_or_recipe_user_id"); + boolean isLinked1 = result.getBoolean("is_linked_or_is_a_primary_user"); + return new AccountLinkingInfo(primaryUserId1, isLinked1); + } + return null; + }); + } + return accountLinkingInfo; + } + + public static void updateTimeJoinedForPrimaryUser_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, String primaryUserId) + throws SQLException, StorageQueryException { + String QUERY = "UPDATE " + getConfig(start).getUsersTable() + + " SET primary_or_recipe_user_time_joined = (SELECT MIN(time_joined) FROM " + + getConfig(start).getUsersTable() + " WHERE app_id = ? AND primary_or_recipe_user_id = ?) WHERE " + + " app_id = ? AND primary_or_recipe_user_id = ?"; + update(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, primaryUserId); + pst.setString(3, appIdentifier.getAppId()); + pst.setString(4, primaryUserId); + }); + } - UserInfoPaginationResultHolder(String userId, String recipeId) { - this.userId = userId; - this.recipeId = recipeId; + private static class AllAuthRecipeUsersResultHolder { + String userId; + String tenantId; + String primaryOrRecipeUserId; + boolean isLinkedOrIsAPrimaryUser; + RECIPE_ID recipeId; + long timeJoined; + + AllAuthRecipeUsersResultHolder(String userId, String tenantId, String primaryOrRecipeUserId, + boolean isLinkedOrIsAPrimaryUser, String recipeId, long timeJoined) { + this.userId = userId.trim(); + this.tenantId = tenantId; + this.primaryOrRecipeUserId = primaryOrRecipeUserId; + this.isLinkedOrIsAPrimaryUser = isLinkedOrIsAPrimaryUser; + this.recipeId = RECIPE_ID.getEnumFromString(recipeId); + this.timeJoined = timeJoined; } } @@ -1147,4 +1781,14 @@ public KeyValueInfo map(ResultSet result) throws Exception { return new KeyValueInfo(result.getString("value"), result.getLong("created_at_time")); } } + + public static class AccountLinkingInfo { + public String primaryUserId; + public boolean isLinked; + + public AccountLinkingInfo(String primaryUserId, boolean isLinked) { + this.primaryUserId = primaryUserId; + this.isLinked = isLinked; + } + } } 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 067e5afe..31858944 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java @@ -17,13 +17,15 @@ package io.supertokens.storage.postgresql.queries; import io.supertokens.pluginInterface.RowMapper; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.authRecipe.LoginMethod; +import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; 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; import io.supertokens.pluginInterface.sqlStorage.SQLStorage.TransactionIsolationLevel; import io.supertokens.storage.postgresql.ConnectionPool; import io.supertokens.storage.postgresql.Start; @@ -36,6 +38,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.*; +import java.util.stream.Collectors; import static io.supertokens.pluginInterface.RECIPE_ID.PASSWORDLESS; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; @@ -55,7 +58,8 @@ public static String getQueryToCreateUsersTable(Start start) { + "time_joined BIGINT NOT NULL, " + "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," + "CONSTRAINT " + Utils.getConstraintName(schema, usersTable, null, "pkey") + " PRIMARY KEY (app_id, user_id)" + ");"; @@ -79,7 +83,8 @@ static String getQueryToCreatePasswordlessUserToTenantTable(Start start) { + " 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" + + " REFERENCES " + Config.getConfig(start).getUsersTable() + + "(app_id, tenant_id, user_id) ON DELETE CASCADE" + ");"; // @formatter:on } @@ -98,7 +103,7 @@ public static String getQueryToCreateDevicesTable(Start start) { + "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," + + " 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)" + ");"; @@ -126,7 +131,8 @@ public static String getQueryToCreateCodesTable(Start start) { + " 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)" + + " REFERENCES " + Config.getConfig(start).getPasswordlessDevicesTable() + + "(app_id, tenant_id, device_id_hash)" + " ON DELETE CASCADE ON UPDATE CASCADE" + ");"; } @@ -138,7 +144,8 @@ public static String getQueryToCreateDeviceEmailIndex(Start start) { public static String getQueryToCreateDevicePhoneNumberIndex(Start start) { return "CREATE INDEX passwordless_devices_phone_number_index ON " - + Config.getConfig(start).getPasswordlessDevicesTable() + " (app_id, tenant_id, phone_number);"; // USING hash + + Config.getConfig(start).getPasswordlessDevicesTable() + + " (app_id, tenant_id, phone_number);"; // USING hash } public static String getQueryToCreateCodeDeviceIdHashIndex(Start start) { @@ -151,8 +158,10 @@ public static String getQueryToCreateCodeCreatedAtIndex(Start start) { + Config.getConfig(start).getPasswordlessCodesTable() + "(app_id, tenant_id, created_at);"; } - public static void createDeviceWithCode(Start start, TenantIdentifier tenantIdentifier, 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 { @@ -197,7 +206,8 @@ public static PasswordlessDevice getDevice_Transaction(Start start, Connection c } public static void incrementDeviceFailedAttemptCount_Transaction(Start start, Connection con, - TenantIdentifier tenantIdentifier, String deviceIdHash) + TenantIdentifier tenantIdentifier, + String deviceIdHash) throws SQLException, StorageQueryException { String QUERY = "UPDATE " + getConfig(start).getPasswordlessDevicesTable() + " SET failed_attempts = failed_attempts + 1" @@ -210,7 +220,8 @@ public static void incrementDeviceFailedAttemptCount_Transaction(Start start, Co }); } - public static void deleteDevice_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, 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 app_id = ? AND tenant_id = ? AND device_id_hash = ?"; @@ -221,7 +232,9 @@ public static void deleteDevice_Transaction(Start start, Connection con, TenantI }); } - public static void deleteDevicesByPhoneNumber_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, @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() @@ -234,7 +247,8 @@ public static void deleteDevicesByPhoneNumber_Transaction(Start start, Connectio }); } - public static void deleteDevicesByPhoneNumber_Transaction(Start start, Connection con, AppIdentifier appIdentifier, @Nonnull String phoneNumber, String userId) + 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() @@ -251,7 +265,8 @@ public static void deleteDevicesByPhoneNumber_Transaction(Start start, Connectio }); } - public static void deleteDevicesByEmail_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, @Nonnull String 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() @@ -264,7 +279,8 @@ public static void deleteDevicesByEmail_Transaction(Start start, Connection con, }); } - public static void deleteDevicesByEmail_Transaction(Start start, Connection con, AppIdentifier appIdentifier, @Nonnull String email, String userId) + 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() @@ -281,7 +297,8 @@ public static void deleteDevicesByEmail_Transaction(Start start, Connection con, }); } - private static void createCode_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, 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() + "(app_id, tenant_id, code_id, device_id_hash, link_code_hash, created_at)" @@ -311,7 +328,9 @@ public static void createCode(Start start, TenantIdentifier tenantIdentifier, Pa }); } - public static PasswordlessCode[] getCodesOfDevice_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, 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 " @@ -335,7 +354,9 @@ public static PasswordlessCode[] getCodesOfDevice_Transaction(Start start, Conne }); } - public static PasswordlessCode getCodeByLinkCodeHash_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, 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 " @@ -354,7 +375,8 @@ public static PasswordlessCode getCodeByLinkCodeHash_Transaction(Start start, Co }); } - public static void deleteCode_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, 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 app_id = ? AND tenant_id = ? AND code_id = ?"; @@ -366,30 +388,35 @@ public static void deleteCode_Transaction(Start start, Connection con, TenantIde }); } - public static UserInfo createUser(Start start, TenantIdentifier tenantIdentifier, String id, @Nullable String email, @Nullable String phoneNumber, long timeJoined) + public static AuthRecipeUserInfo createUser(Start start, TenantIdentifier tenantIdentifier, String id, @Nullable String email, + @Nullable String phoneNumber, long timeJoined) throws StorageTransactionLogicException, StorageQueryException { return 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, recipe_id)" + " VALUES(?, ?, ?)"; + + "(app_id, user_id, primary_or_recipe_user_id, recipe_id)" + " VALUES(?, ?, ?, ?)"; update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, id); - pst.setString(3, PASSWORDLESS.toString()); + pst.setString(3, id); + pst.setString(4, PASSWORDLESS.toString()); }); } { // all_auth_recipe_users String QUERY = "INSERT INTO " + getConfig(start).getUsersTable() - + "(app_id, tenant_id, user_id, recipe_id, time_joined)" + " VALUES(?, ?, ?, ?, ?)"; + + "(app_id, tenant_id, user_id, primary_or_recipe_user_id, recipe_id, time_joined, primary_or_recipe_user_time_joined)" + + " VALUES(?, ?, ?, ?, ?, ?, ?)"; update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, tenantIdentifier.getTenantId()); pst.setString(3, id); - pst.setString(4, PASSWORDLESS.toString()); - pst.setLong(5, timeJoined); + pst.setString(4, id); + pst.setString(5, PASSWORDLESS.toString()); + pst.setLong(6, timeJoined); + pst.setLong(7, timeJoined); }); } @@ -417,16 +444,20 @@ public static UserInfo createUser(Start start, TenantIdentifier tenantIdentifier pst.setString(5, phoneNumber); }); } - UserInfo userInfo = userInfoWithTenantIds_transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), new UserInfoPartial(id, email, phoneNumber, timeJoined)); + UserInfoPartial userInfo = new UserInfoPartial(id, email, phoneNumber, timeJoined); + fillUserInfoWithTenantIds_transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), userInfo); + fillUserInfoWithVerified_transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), userInfo); sqlCon.commit(); - return userInfo; + return AuthRecipeUserInfo.create(id, false, + userInfo.toLoginMethod()); } catch (SQLException throwables) { throw new StorageTransactionLogicException(throwables); } }); } - private static UserInfoWithTenantId[] getUserInfosWithTenant(Start start, Connection con, AppIdentifier appIdentifier, String userId) + private static UserInfoWithTenantId[] getUserInfosWithTenant_Transaction(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 " @@ -453,49 +484,59 @@ private static UserInfoWithTenantId[] getUserInfosWithTenant(Start start, Connec }); } - 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); + public static void deleteUser_Transaction(Connection sqlCon, Start start, AppIdentifier appIdentifier, + String userId, boolean deleteUserIdMappingToo) + throws StorageQueryException, SQLException { + UserInfoWithTenantId[] userInfos = getUserInfosWithTenant_Transaction(start, sqlCon, appIdentifier, userId); - { - String QUERY = "DELETE FROM " + getConfig(start).getAppIdToUserIdTable() - + " WHERE app_id = ? AND user_id = ?"; + if (deleteUserIdMappingToo) { + String QUERY = "DELETE FROM " + getConfig(start).getAppIdToUserIdTable() + + " WHERE app_id = ? AND user_id = ?"; - update(sqlCon, QUERY, pst -> { - pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, userId); - }); - } + update(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); + } else { + { + String QUERY = "DELETE FROM " + getConfig(start).getUsersTable() + + " WHERE app_id = ? AND user_id = ?"; + update(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); + } - for (UserInfoWithTenantId userInfo : userInfos) { - if (userInfo.email != null) { - deleteDevicesByEmail_Transaction(start, sqlCon, - new TenantIdentifier( - appIdentifier.getConnectionUriDomain(), appIdentifier.getAppId(), - userInfo.tenantId), - userInfo.email); - } - if (userInfo.phoneNumber != null) { - deleteDevicesByPhoneNumber_Transaction(start, sqlCon, - new TenantIdentifier( - appIdentifier.getConnectionUriDomain(), appIdentifier.getAppId(), - userInfo.tenantId), - userInfo.phoneNumber); - } - } + { + String QUERY = "DELETE FROM " + getConfig(start).getPasswordlessUsersTable() + + " WHERE app_id = ? AND user_id = ?"; + update(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); + } + } - sqlCon.commit(); - } catch (SQLException throwables) { - throw new StorageTransactionLogicException(throwables); + for (UserInfoWithTenantId userInfo : userInfos) { + if (userInfo.email != null) { + deleteDevicesByEmail_Transaction(start, sqlCon, + new TenantIdentifier( + appIdentifier.getConnectionUriDomain(), appIdentifier.getAppId(), + userInfo.tenantId), + userInfo.email); } - return null; - }); + if (userInfo.phoneNumber != null) { + deleteDevicesByPhoneNumber_Transaction(start, sqlCon, + new TenantIdentifier( + appIdentifier.getConnectionUriDomain(), appIdentifier.getAppId(), + userInfo.tenantId), + userInfo.phoneNumber); + } + } } - public static int updateUserEmail_Transaction(Start start, Connection con, AppIdentifier appIdentifier, 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).getPasswordlessUserToTenantTable() @@ -519,7 +560,8 @@ public static int updateUserEmail_Transaction(Start start, Connection con, AppId } } - public static int updateUserPhoneNumber_Transaction(Start start, Connection con, AppIdentifier appIdentifier, 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).getPasswordlessUserToTenantTable() @@ -562,7 +604,8 @@ public static PasswordlessDevice getDevice(Start start, TenantIdentifier tenantI } } - public static PasswordlessDevice[] getDevicesByEmail(Start start, TenantIdentifier tenantIdentifier, @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() @@ -609,7 +652,8 @@ public static PasswordlessDevice[] getDevicesByPhoneNumber(Start start, TenantId }); } - public static PasswordlessCode[] getCodesOfDevice(Start start, TenantIdentifier tenantIdentifier, 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. @@ -617,7 +661,8 @@ public static PasswordlessCode[] getCodesOfDevice(Start start, TenantIdentifier } } - public static PasswordlessCode[] getCodesBefore(Start start, TenantIdentifier tenantIdentifier, 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 app_id = ? AND tenant_id = ? AND created_at < ?"; @@ -639,7 +684,8 @@ public static PasswordlessCode[] getCodesBefore(Start start, TenantIdentifier te }); } - public static PasswordlessCode getCode(Start start, TenantIdentifier tenantIdentifier, 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 app_id = ? AND tenant_id = ? AND code_id = ?"; @@ -656,7 +702,8 @@ public static PasswordlessCode getCode(Start start, TenantIdentifier tenantIdent }); } - public static PasswordlessCode getCodeByLinkCodeHash(Start start, TenantIdentifier tenantIdentifier, 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. @@ -664,28 +711,52 @@ public static PasswordlessCode getCodeByLinkCodeHash(Start start, TenantIdentifi } } - public static List getUsersByIdList(Start start, AppIdentifier appIdentifier, List ids) + public static List getUsersInfoUsingIdList(Start start, Set ids, + AppIdentifier appIdentifier) 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, email, phone_number, time_joined " - + "FROM " + getConfig(start).getPasswordlessUsersTable()); - QUERY.append(" WHERE app_id = ? AND user_id IN ("); - for (int i = 0; i < ids.size(); i++) { - QUERY.append("?"); - if (i != ids.size() - 1) { - // not the last element - QUERY.append(","); + String QUERY = "SELECT user_id, email, phone_number, time_joined " + + "FROM " + getConfig(start).getPasswordlessUsersTable() + " WHERE user_id IN (" + + Utils.generateCommaSeperatedQuestionMarks(ids.size()) + ") AND app_id = ?"; + + List userInfos = execute(start, QUERY, pst -> { + int index = 1; + for (String id : ids) { + pst.setString(index, id); + index++; } - } - QUERY.append(")"); + pst.setString(index, appIdentifier.getAppId()); + }, result -> { + List finalResult = new ArrayList<>(); + while (result.next()) { + finalResult.add(UserInfoRowMapper.getInstance().mapOrThrow(result)); + } + return finalResult; + }); + fillUserInfoWithTenantIds(start, appIdentifier, userInfos); + fillUserInfoWithVerified(start, appIdentifier, userInfos); + return userInfos.stream().map(UserInfoPartial::toLoginMethod).collect(Collectors.toList()); + } + return Collections.emptyList(); + } - List userInfos = execute(start, QUERY.toString(), pst -> { - pst.setString(1, appIdentifier.getAppId()); - for (int i = 0; i < ids.size(); i++) { - // i+2 cause this starts with 1 and not 0, 1 is appId - pst.setString(i + 2, ids.get(i)); + public static List getUsersInfoUsingIdList_Transaction(Start start, Connection con, Set ids, + AppIdentifier appIdentifier) + throws SQLException, StorageQueryException { + if (ids.size() > 0) { + // No need to filter based on tenantId because the id list is already filtered for a tenant + String QUERY = "SELECT user_id, email, phone_number, time_joined " + + "FROM " + getConfig(start).getPasswordlessUsersTable() + " WHERE user_id IN (" + + Utils.generateCommaSeperatedQuestionMarks(ids.size()) + ") AND app_id = ?"; + + List userInfos = execute(con, QUERY, pst -> { + int index = 1; + for (String id : ids) { + pst.setString(index, id); + index++; } + pst.setString(index, appIdentifier.getAppId()); }, result -> { List finalResult = new ArrayList<>(); while (result.next()) { @@ -693,16 +764,22 @@ public static List getUsersByIdList(Start start, AppIdentifier appIden } return finalResult; }); - return userInfoWithTenantIds(start, appIdentifier, userInfos); + fillUserInfoWithTenantIds_transaction(start, con, appIdentifier, userInfos); + fillUserInfoWithVerified_transaction(start, con, appIdentifier, userInfos); + return userInfos.stream().map(UserInfoPartial::toLoginMethod).collect(Collectors.toList()); } return Collections.emptyList(); } - public static UserInfo getUserById(Start start, AppIdentifier appIdentifier, String userId) throws StorageQueryException, SQLException { + private static UserInfoPartial getUserById_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, + String userId) + throws StorageQueryException, SQLException { + // we don't need a LOCK 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 = ?"; - UserInfoPartial userInfo = execute(start, QUERY, pst -> { + return execute(sqlCon, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); pst.setString(2, userId); }, result -> { @@ -711,92 +788,166 @@ public static UserInfo getUserById(Start start, AppIdentifier appIdentifier, Str } return null; }); - return userInfoWithTenantIds(start, appIdentifier, userInfo); } - 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() - + " WHERE app_id = ? AND user_id = ?"; + public static List lockEmail_Transaction(Start start, Connection con, AppIdentifier appIdentifier, + String email) 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 FROM " + getConfig(start).getPasswordlessUsersTable() + + " WHERE app_id = ? AND email = ? FOR UPDATE"; - return execute(sqlCon, QUERY, pst -> { + return execute(con, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, userId); + pst.setString(2, email); }, result -> { - if (result.next()) { - return UserInfoRowMapper.getInstance().mapOrThrow(result); + List userIds = new ArrayList<>(); + while (result.next()) { + userIds.add(result.getString("user_id")); } - return null; + return userIds; + }); + } + + public static List lockPhoneAndTenant_Transaction(Start start, Connection con, + AppIdentifier appIdentifier, + String phoneNumber) + throws SQLException, StorageQueryException { + + String QUERY = "SELECT user_id FROM " + getConfig(start).getPasswordlessUsersTable() + + " WHERE app_id = ? AND phone_number = ? FOR UPDATE"; + return execute(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, phoneNumber); + }, result -> { + List userIds = new ArrayList<>(); + while (result.next()) { + userIds.add(result.getString("user_id")); + } + return userIds; }); } - public static UserInfo getUserByEmail(Start start, TenantIdentifier tenantIdentifier, @Nonnull String email) + public static String getPrimaryUserIdUsingEmail(Start start, TenantIdentifier tenantIdentifier, + String email) 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.email = ? "; + String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id " + + "FROM " + getConfig(start).getPasswordlessUserToTenantTable() + " AS pless" + + " JOIN " + getConfig(start).getUsersTable() + " AS all_users" + + " ON pless.app_id = all_users.app_id AND pless.user_id = all_users.user_id" + + " WHERE pless.app_id = ? AND pless.tenant_id = ? AND pless.email = ?"; - UserInfoPartial userInfo = execute(start, QUERY, pst -> { + 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 result.getString("user_id"); } return null; }); - return userInfoWithTenantIds(start, tenantIdentifier.toAppIdentifier(), userInfo); } - public static UserInfo getUserByPhoneNumber(Start start, TenantIdentifier tenantIdentifier, @Nonnull String phoneNumber) + public static List getPrimaryUserIdsUsingEmail_Transaction(Start start, Connection con, AppIdentifier appIdentifier, + String email) 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 = ? "; + String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id " + + "FROM " + getConfig(start).getPasswordlessUsersTable() + " AS pless" + + " JOIN " + getConfig(start).getAppIdToUserIdTable() + " AS all_users" + + " ON pless.app_id = all_users.app_id AND pless.user_id = all_users.user_id" + + " WHERE pless.app_id = ? AND pless.email = ?"; - UserInfoPartial userInfo = execute(start, QUERY, pst -> { + return execute(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, email); + }, result -> { + List userIds = new ArrayList<>(); + while (result.next()) { + userIds.add(result.getString("user_id")); + } + return userIds; + }); + } + + public static String getPrimaryUserByPhoneNumber(Start start, TenantIdentifier tenantIdentifier, + @Nonnull String phoneNumber) + throws StorageQueryException, SQLException { + String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id " + + "FROM " + getConfig(start).getPasswordlessUserToTenantTable() + " AS pless" + + " JOIN " + getConfig(start).getUsersTable() + " AS all_users" + + " ON pless.app_id = all_users.app_id AND pless.user_id = all_users.user_id" + + " WHERE pless.app_id = ? AND pless.tenant_id = ? AND pless.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); + return result.getString("user_id"); } return null; }); - return userInfoWithTenantIds(start, tenantIdentifier.toAppIdentifier(), userInfo); } - public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId) + public static List listUserIdsByPhoneNumber_Transaction(Start start, Connection con, AppIdentifier appIdentifier, + @Nonnull String phoneNumber) throws StorageQueryException, SQLException { - UserInfoPartial userInfo = PasswordlessQueries.getUserById(start, sqlCon, + String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id " + + "FROM " + getConfig(start).getPasswordlessUsersTable() + " AS pless" + + " JOIN " + getConfig(start).getUsersTable() + " AS all_users" + + " ON pless.app_id = all_users.app_id AND pless.user_id = all_users.user_id" + + " WHERE pless.app_id = ? AND pless.phone_number = ?"; + + return execute(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, phoneNumber); + }, result -> { + List userIds = new ArrayList<>(); + while (result.next()) { + userIds.add(result.getString("user_id")); + } + return userIds; + }); + } + + public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlCon, + TenantIdentifier tenantIdentifier, String userId) + throws StorageQueryException, SQLException, UnknownUserIdException { + UserInfoPartial userInfo = PasswordlessQueries.getUserById_Transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), userId); + if (userInfo == null) { + throw new UnknownUserIdException(); + } + + GeneralQueries.AccountLinkingInfo accountLinkingInfo = GeneralQueries.getAccountLinkingInfo_Transaction(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"; + + "(app_id, tenant_id, user_id, primary_or_recipe_user_id, is_linked_or_is_a_primary_user, recipe_id, time_joined, primary_or_recipe_user_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); + pst.setString(4, accountLinkingInfo.primaryUserId); + pst.setBoolean(5, accountLinkingInfo.isLinked); + pst.setString(6, PASSWORDLESS.toString()); + pst.setLong(7, userInfo.timeJoined); + pst.setLong(8, userInfo.timeJoined); }); + + GeneralQueries.updateTimeJoinedForPrimaryUser_Transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), accountLinkingInfo.primaryUserId); } { // 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"; + + " VALUES(?, ?, ?, ?, ?)" + " ON CONFLICT ON CONSTRAINT " + + Utils.getConstraintName(Config.getConfig(start).getTableSchema(), getConfig(start).getPasswordlessUserToTenantTable(), null, "pkey") + + " DO NOTHING"; int numRows = update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); @@ -810,7 +961,8 @@ public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlC } } - public static boolean removeUserIdFromTenant_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId) + 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() @@ -828,42 +980,124 @@ 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, AppIdentifier appIdentifier, UserInfoPartial userInfo) + private static UserInfoPartial fillUserInfoWithVerified_transaction(Start start, + Connection sqlCon, + AppIdentifier appIdentifier, + UserInfoPartial userInfo) throws SQLException, StorageQueryException { if (userInfo == null) return null; - try (Connection con = ConnectionPool.getConnection(start)) { - return userInfoWithTenantIds_transaction(start, con, appIdentifier, Arrays.asList(userInfo)).get(0); + return fillUserInfoWithVerified_transaction(start, sqlCon, appIdentifier, List.of(userInfo)).get(0); + } + + private static List fillUserInfoWithVerified_transaction(Start start, + Connection sqlCon, + AppIdentifier appIdentifier, + List userInfos) + throws SQLException, StorageQueryException { + List userIdsAndEmails = new ArrayList<>(); + for (UserInfoPartial userInfo : userInfos) { + if (userInfo.email == null) { + // phone number, so we mark it as verified + userInfo.verified = true; + } else { + userIdsAndEmails.add(new EmailVerificationQueries.UserIdAndEmail(userInfo.id, userInfo.email)); + } + } + List userIdsThatAreVerified = EmailVerificationQueries.isEmailVerified_transaction(start, sqlCon, + appIdentifier, + userIdsAndEmails); + Set verifiedUserIdsSet = new HashSet<>(userIdsThatAreVerified); + for (UserInfoPartial userInfo : userInfos) { + if (userInfo.verified != null) { + // this means phone number + assert (userInfo.email == null); + continue; + } + if (verifiedUserIdsSet.contains(userInfo.id)) { + userInfo.verified = true; + } else { + userInfo.verified = false; + } } + return userInfos; } - private static List userInfoWithTenantIds(Start start, AppIdentifier appIdentifier, List userInfos) + private static List fillUserInfoWithVerified(Start start, + AppIdentifier appIdentifier, + List userInfos) throws SQLException, StorageQueryException { - try (Connection con = ConnectionPool.getConnection(start)) { - return userInfoWithTenantIds_transaction(start, con, appIdentifier, userInfos); + List userIdsAndEmails = new ArrayList<>(); + for (UserInfoPartial userInfo : userInfos) { + if (userInfo.email == null) { + // phone number, so we mark it as verified + userInfo.verified = true; + } else { + userIdsAndEmails.add(new EmailVerificationQueries.UserIdAndEmail(userInfo.id, userInfo.email)); + } } + List userIdsThatAreVerified = EmailVerificationQueries.isEmailVerified(start, + appIdentifier, + userIdsAndEmails); + Set verifiedUserIdsSet = new HashSet<>(userIdsThatAreVerified); + for (UserInfoPartial userInfo : userInfos) { + if (userInfo.verified != null) { + // this means phone number + assert (userInfo.email == null); + continue; + } + if (verifiedUserIdsSet.contains(userInfo.id)) { + userInfo.verified = true; + } else { + userInfo.verified = false; + } + } + return userInfos; } - private static UserInfo userInfoWithTenantIds_transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, UserInfoPartial userInfo) + private static UserInfoPartial fillUserInfoWithTenantIds_transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + UserInfoPartial userInfo) throws SQLException, StorageQueryException { if (userInfo == null) return null; - return userInfoWithTenantIds_transaction(start, sqlCon, appIdentifier, Arrays.asList(userInfo)).get(0); + return fillUserInfoWithTenantIds_transaction(start, sqlCon, appIdentifier, List.of(userInfo)).get(0); } - private static List userInfoWithTenantIds_transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, List userInfos) + private static List fillUserInfoWithTenantIds_transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + 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, appIdentifier, userIds); - List result = new ArrayList<>(); + Map> tenantIdsForUserIds = GeneralQueries.getTenantIdsForUserIds_transaction(start, sqlCon, + appIdentifier, + 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]))); + userInfo.tenantIds = tenantIdsForUserIds.get(userInfo.id).toArray(new String[0]); + } + return userInfos; + } + + private static List fillUserInfoWithTenantIds(Start start, + AppIdentifier appIdentifier, + 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; } - return result; + Map> tenantIdsForUserIds = GeneralQueries.getTenantIdsForUserIds(start, + appIdentifier, + userIds); + List result = new ArrayList<>(); + for (UserInfoPartial userInfo : userInfos) { + userInfo.tenantIds = tenantIdsForUserIds.get(userInfo.id).toArray(new String[0]); + } + return userInfos; } private static class PasswordlessDeviceRowMapper implements RowMapper { @@ -906,6 +1140,9 @@ private static class UserInfoPartial { public final long timeJoined; public final String email; public final String phoneNumber; + public String[] tenantIds; + public Boolean verified; + public Boolean isPrimary; UserInfoPartial(String id, @Nullable String email, @Nullable String phoneNumber, long timeJoined) { this.id = id.trim(); @@ -918,6 +1155,13 @@ private static class UserInfoPartial { this.email = email; this.phoneNumber = phoneNumber; } + + public LoginMethod toLoginMethod() { + assert (tenantIds != null); + assert (verified != null); + return new LoginMethod(id, timeJoined, verified, new LoginMethod.PasswordlessInfo(email, phoneNumber), + tenantIds); + } } private static class UserInfoRowMapper implements RowMapper { @@ -937,7 +1181,6 @@ public UserInfoPartial map(ResultSet result) throws Exception { } } - private static class UserInfoWithTenantId { public final String userId; public final String tenantId; 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 928fbd66..d6685638 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/SessionQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/SessionQueries.java @@ -124,19 +124,44 @@ public static void createNewSession(Start start, TenantIdentifier tenantIdentifi 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, use_static_key FROM " + getConfig(start).getSessionInfoTable() - + " WHERE app_id = ? AND tenant_id = ? AND session_handle = ? FOR UPDATE"; - return execute(con, QUERY, pst -> { + // we do this as two separate queries and not one query with left join cause psql does not + // support left join with for update if the right table returns null. + + String QUERY = + "SELECT session_handle, user_id, refresh_token_hash_2, session_data, " + + "expires_at, created_at_time, jwt_user_payload, use_static_key FROM " + + getConfig(start).getSessionInfoTable() + + " WHERE app_id = ? AND tenant_id = ? AND session_handle = ? FOR UPDATE"; + SessionInfo sessionInfo = 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); + return SessionInfoRowMapper.getInstance().mapOrThrow(result, false); } return null; }); + + if (sessionInfo == null) { + return null; + } + + QUERY = "SELECT primary_or_recipe_user_id FROM " + getConfig(start).getUsersTable() + + " WHERE app_id = ? AND user_id = ?"; + + return execute(con, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, sessionInfo.recipeUserId); + }, result -> { + if (result.next()) { + String primaryUserId = result.getString("primary_or_recipe_user_id"); + if (primaryUserId != null) { + sessionInfo.userId = primaryUserId; + } + } + return sessionInfo; + }); } public static void updateSessionInfo_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, @@ -208,6 +233,18 @@ public static void deleteSessionsOfUser(Start start, AppIdentifier appIdentifier }); } + public static void deleteSessionsOfUser_Transaction(Connection sqlCon, Start start, AppIdentifier appIdentifier, + String userId) + throws SQLException, StorageQueryException { + String QUERY = "DELETE FROM " + getConfig(start).getSessionInfoTable() + + " WHERE app_id = ? AND user_id = ?"; + + update(sqlCon, QUERY.toString(), pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); + } + public static boolean deleteSessionsOfUser(Start start, TenantIdentifier tenantIdentifier, String userId) throws SQLException, StorageQueryException { String QUERY = "DELETE FROM " + getConfig(start).getSessionInfoTable() @@ -311,16 +348,24 @@ public static int updateSession(Start start, TenantIdentifier tenantIdentifier, 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, use_static_key FROM " + getConfig(start).getSessionInfoTable() - + " WHERE app_id = ? AND tenant_id = ? AND session_handle = ?"; + String QUERY = + "SELECT sess.session_handle, sess.user_id, sess.refresh_token_hash_2, sess.session_data, sess" + + ".expires_at, " + + + "sess.created_at_time, sess.jwt_user_payload, sess.use_static_key, users" + + ".primary_or_recipe_user_id FROM " + + getConfig(start).getSessionInfoTable() + + " AS sess LEFT JOIN " + getConfig(start).getUsersTable() + + " as users ON sess.app_id = users.app_id AND sess.user_id = users.user_id WHERE sess.app_id =" + + " ? AND " + + "sess.tenant_id = ? AND sess.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); + return SessionInfoRowMapper.getInstance().mapOrThrow(result, true); } return null; }); @@ -372,7 +417,7 @@ public static void removeAccessTokenSigningKeysBefore(Start start, AppIdentifier }); } - static class SessionInfoRowMapper implements RowMapper { + static class SessionInfoRowMapper { public static final SessionInfoRowMapper INSTANCE = new SessionInfoRowMapper(); private SessionInfoRowMapper() { @@ -382,14 +427,23 @@ private static SessionInfoRowMapper getInstance() { return INSTANCE; } - @Override - public SessionInfo map(ResultSet result) throws Exception { + public SessionInfo mapOrThrow(ResultSet result, boolean hasPrimaryOrRecipeUserId) throws StorageQueryException { JsonParser jp = new JsonParser(); - return new SessionInfo(result.getString("session_handle"), result.getString("user_id"), - 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.getBoolean("use_static_key")); + // if result.getString("primary_or_recipe_user_id") is null, it will be handled by SessionInfo + // constructor + try { + return new SessionInfo(result.getString("session_handle"), + hasPrimaryOrRecipeUserId ? result.getString("primary_or_recipe_user_id") : + result.getString("user_id"), + result.getString("user_id"), + 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.getBoolean("use_static_key")); + } catch (Exception e) { + throw new StorageQueryException(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 d904b7b1..2a37c9dc 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/ThirdPartyQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/ThirdPartyQueries.java @@ -17,21 +17,25 @@ package io.supertokens.storage.postgresql.queries; import io.supertokens.pluginInterface.RowMapper; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.authRecipe.LoginMethod; +import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; +import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; 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.pluginInterface.thirdparty.exception.DuplicateThirdPartyUserException; 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; -import org.jetbrains.annotations.NotNull; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.util.*; +import java.util.stream.Collectors; import static io.supertokens.pluginInterface.RECIPE_ID.THIRD_PARTY; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; @@ -53,7 +57,8 @@ static String getQueryToCreateUsersTable(Start start) { + "time_joined BIGINT NOT NULL," + "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," + + " 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)" + ");"; @@ -80,41 +85,48 @@ static String getQueryToCreateThirdPartyUserToTenantTable(Start start) { + "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") + + "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" + + " REFERENCES " + Config.getConfig(start).getUsersTable() + + "(app_id, tenant_id, user_id) ON DELETE CASCADE" + ");"; // @formatter:on } - public static UserInfo signUp(Start start, TenantIdentifier tenantIdentifier, String id, String email, UserInfo.ThirdParty thirdParty, long timeJoined) + public static AuthRecipeUserInfo signUp(Start start, TenantIdentifier tenantIdentifier, String id, String email, + LoginMethod.ThirdParty thirdParty, long timeJoined) throws StorageQueryException, StorageTransactionLogicException { return 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, recipe_id)" + " VALUES(?, ?, ?)"; + + "(app_id, user_id, primary_or_recipe_user_id, recipe_id)" + " VALUES(?, ?, ?, ?)"; update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, id); - pst.setString(3, THIRD_PARTY.toString()); + pst.setString(3, id); + pst.setString(4, THIRD_PARTY.toString()); }); } { // all_auth_recipe_users String QUERY = "INSERT INTO " + getConfig(start).getUsersTable() - + "(app_id, tenant_id, user_id, recipe_id, time_joined)" + " VALUES(?, ?, ?, ?, ?)"; + + "(app_id, tenant_id, user_id, primary_or_recipe_user_id, recipe_id, time_joined, primary_or_recipe_user_time_joined)" + + " VALUES(?, ?, ?, ?, ?, ?, ?)"; update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, tenantIdentifier.getTenantId()); pst.setString(3, id); - pst.setString(4, THIRD_PARTY.toString()); - pst.setLong(5, timeJoined); + pst.setString(4, id); + pst.setString(5, THIRD_PARTY.toString()); + pst.setLong(6, timeJoined); + pst.setLong(7, timeJoined); }); } @@ -145,9 +157,11 @@ public static UserInfo signUp(Start start, TenantIdentifier tenantIdentifier, St }); } - UserInfo userInfo = userInfoWithTenantIds_transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), new UserInfoPartial(id, email, thirdParty, timeJoined)); + UserInfoPartial userInfo = new UserInfoPartial(id, email, thirdParty, timeJoined); + fillUserInfoWithTenantIds_transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), userInfo); + fillUserInfoWithVerified_transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), userInfo); sqlCon.commit(); - return userInfo; + return AuthRecipeUserInfo.create(id, false, userInfo.toLoginMethod()); } catch (SQLException throwables) { throw new StorageTransactionLogicException(throwables); @@ -155,69 +169,125 @@ public static UserInfo signUp(Start start, TenantIdentifier tenantIdentifier, St }); } - 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).getAppIdToUserIdTable() - + " WHERE app_id = ? AND user_id = ?"; + public static void deleteUser_Transaction(Connection sqlCon, Start start, AppIdentifier appIdentifier, + String userId, boolean deleteUserIdMappingToo) + throws StorageQueryException, SQLException { + if (deleteUserIdMappingToo) { + String QUERY = "DELETE FROM " + getConfig(start).getAppIdToUserIdTable() + + " WHERE app_id = ? AND user_id = ?"; - update(sqlCon, QUERY, pst -> { - pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, userId); - }); - } + update(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); + } else { + { + String QUERY = "DELETE FROM " + getConfig(start).getUsersTable() + + " WHERE app_id = ? AND user_id = ?"; + update(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); + } - sqlCon.commit(); - } catch (SQLException throwables) { - throw new StorageTransactionLogicException(throwables); + { + String QUERY = "DELETE FROM " + getConfig(start).getThirdPartyUsersTable() + + " WHERE app_id = ? AND user_id = ?"; + update(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); } - return null; + } + } + + public static List lockEmail_Transaction(Start start, Connection con, + AppIdentifier appIdentifier, + String email) throws SQLException, StorageQueryException { + String QUERY = "SELECT tp.user_id as user_id " + + "FROM " + getConfig(start).getThirdPartyUsersTable() + " AS tp" + + " WHERE tp.app_id = ? AND tp.email = ? FOR UPDATE"; + + return execute(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, email); + }, result -> { + List finalResult = new ArrayList<>(); + while (result.next()) { + finalResult.add(result.getString("user_id")); + } + return finalResult; }); } - public static UserInfo getThirdPartyUserInfoUsingId(Start start, AppIdentifier appIdentifier, String userId) + public static List lockThirdPartyInfoAndTenant_Transaction(Start start, Connection con, + 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 = ?"; + String QUERY = "SELECT user_id " + + " FROM " + getConfig(start).getThirdPartyUsersTable() + + " WHERE app_id = ? AND third_party_id = ? AND third_party_user_id = ? FOR UPDATE"; - UserInfoPartial userInfo = execute(start, QUERY.toString(), pst -> { + return execute(con, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, userId); + pst.setString(2, thirdPartyId); + pst.setString(3, thirdPartyUserId); }, result -> { - if (result.next()) { - return UserInfoRowMapper.getInstance().mapOrThrow(result); + List finalResult = new ArrayList<>(); + while (result.next()) { + finalResult.add(result.getString("user_id")); } - return null; + return finalResult; }); - return userInfoWithTenantIds(start, appIdentifier, userInfo); } - public static List getUsersInfoUsingIdList(Start start, AppIdentifier appIdentifier, List ids) + public static List getUsersInfoUsingIdList(Start start, Set ids, + AppIdentifier appIdentifier) 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()); - QUERY.append(" WHERE app_id = ? AND user_id IN ("); - for (int i = 0; i < ids.size(); i++) { - QUERY.append("?"); - if (i != ids.size() - 1) { - // not the last element - QUERY.append(","); + String QUERY = "SELECT user_id, third_party_id, third_party_user_id, email, time_joined " + + "FROM " + getConfig(start).getThirdPartyUsersTable() + " WHERE user_id IN (" + + Utils.generateCommaSeperatedQuestionMarks(ids.size()) + ") AND app_id = ?"; + + List userInfos = execute(start, QUERY, pst -> { + int index = 1; + for (String id : ids) { + pst.setString(index, id); + index++; } + pst.setString(index, appIdentifier.getAppId()); + }, result -> { + List finalResult = new ArrayList<>(); + while (result.next()) { + finalResult.add(UserInfoRowMapper.getInstance().mapOrThrow(result)); + } + return finalResult; + }); + + try (Connection con = ConnectionPool.getConnection(start)) { + fillUserInfoWithTenantIds_transaction(start, con, appIdentifier, userInfos); + fillUserInfoWithVerified_transaction(start, con, appIdentifier, userInfos); } - QUERY.append(")"); + return userInfos.stream().map(UserInfoPartial::toLoginMethod).collect(Collectors.toList()); + } + return Collections.emptyList(); + } - List userInfos = execute(start, QUERY.toString(), pst -> { - pst.setString(1, appIdentifier.getAppId()); - for (int i = 0; i < ids.size(); i++) { - // i+2 cause this starts with 1 and not 0, and 1 is appId - pst.setString(i + 2, ids.get(i)); + public static List getUsersInfoUsingIdList_Transaction(Start start, Connection con, Set ids, + AppIdentifier appIdentifier) + throws SQLException, StorageQueryException { + if (ids.size() > 0) { + String QUERY = "SELECT user_id, third_party_id, third_party_user_id, email, time_joined " + + "FROM " + getConfig(start).getThirdPartyUsersTable() + " WHERE user_id IN (" + + Utils.generateCommaSeperatedQuestionMarks(ids.size()) + ") AND app_id = ?"; + + List userInfos = execute(con, QUERY, pst -> { + int index = 1; + for (String id : ids) { + pst.setString(index, id); + index++; } + pst.setString(index, appIdentifier.getAppId()); }, result -> { List finalResult = new ArrayList<>(); while (result.next()) { @@ -225,36 +295,82 @@ public static List getUsersInfoUsingIdList(Start start, AppIdentifier } return finalResult; }); - return userInfoWithTenantIds(start, appIdentifier, userInfos); + + fillUserInfoWithTenantIds_transaction(start, con, appIdentifier, userInfos); + fillUserInfoWithVerified_transaction(start, con, appIdentifier, userInfos); + return userInfos.stream().map(UserInfoPartial::toLoginMethod).collect(Collectors.toList()); } return Collections.emptyList(); } - public static UserInfo getThirdPartyUserInfoUsingId(Start start, TenantIdentifier tenantIdentifier, - String thirdPartyId, String thirdPartyUserId) + + public static List listUserIdsByThirdPartyInfo(Start start, AppIdentifier appIdentifier, + String thirdPartyId, String thirdPartyUserId) throws SQLException, StorageQueryException { - 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 = ?"; + String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id " + + "FROM " + getConfig(start).getThirdPartyUsersTable() + " AS tp" + + " JOIN " + getConfig(start).getUsersTable() + " AS all_users" + + " ON tp.app_id = all_users.app_id AND tp.user_id = all_users.user_id" + + " WHERE tp.app_id = ? AND tp.third_party_id = ? AND tp.third_party_user_id = ?"; - UserInfoPartial userInfo = execute(start, QUERY, pst -> { + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, thirdPartyId); + pst.setString(3, thirdPartyUserId); + }, result -> { + List userIds = new ArrayList<>(); + while (result.next()) { + userIds.add(result.getString("user_id")); + } + return userIds; + }); + } + + public static List listUserIdsByThirdPartyInfo_Transaction(Start start, Connection con, AppIdentifier appIdentifier, + String thirdPartyId, String thirdPartyUserId) + throws SQLException, StorageQueryException { + + String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id " + + "FROM " + getConfig(start).getThirdPartyUsersTable() + " AS tp" + + " JOIN " + getConfig(start).getUsersTable() + " AS all_users" + + " ON tp.app_id = all_users.app_id AND tp.user_id = all_users.user_id" + + " WHERE tp.app_id = ? AND tp.third_party_id = ? AND tp.third_party_user_id = ?"; + + return execute(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, thirdPartyId); + pst.setString(3, thirdPartyUserId); + }, result -> { + List userIds = new ArrayList<>(); + while (result.next()) { + userIds.add(result.getString("user_id")); + } + return userIds; + }); + } + + public static String getUserIdByThirdPartyInfo(Start start, TenantIdentifier tenantIdentifier, + String thirdPartyId, String thirdPartyUserId) + throws SQLException, StorageQueryException { + + String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id " + + "FROM " + getConfig(start).getThirdPartyUserToTenantTable() + " AS tp" + + " JOIN " + getConfig(start).getUsersTable() + " AS all_users" + + " ON tp.app_id = all_users.app_id AND tp.user_id = all_users.user_id" + + " WHERE tp.app_id = ? AND tp.tenant_id = ? AND tp.third_party_id = ? AND tp.third_party_user_id = ?"; + + return execute(start, QUERY, pst -> { 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); + return result.getString("user_id"); } return null; }); - return userInfoWithTenantIds(start, tenantIdentifier.toAppIdentifier(), userInfo); } public static void updateUserEmail_Transaction(Start start, Connection con, AppIdentifier appIdentifier, @@ -271,32 +387,12 @@ public static void updateUserEmail_Transaction(Start start, Connection con, AppI }); } - public static UserInfo getUserInfoUsingId_Transaction(Start start, Connection con, - 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 third_party_id = ? AND third_party_user_id = ? FOR UPDATE"; - UserInfoPartial userInfo = execute(con, QUERY, pst -> { - pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, thirdPartyId); - pst.setString(3, thirdPartyUserId); - }, result -> { - if (result.next()) { - return UserInfoRowMapper.getInstance().mapOrThrow(result); - } - return null; - }); - return userInfoWithTenantIds_transaction(start, con, appIdentifier, userInfo); - } - - private static UserInfoPartial getUserInfoUsingUserId(Start start, Connection con, + private static UserInfoPartial getUserInfoUsingUserId_Transaction(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 + // we don't need a LOCK 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 = ?"; @@ -311,55 +407,87 @@ private static UserInfoPartial getUserInfoUsingUserId(Start start, Connection co }); } - public static UserInfo[] getThirdPartyUsersByEmail(Start start, TenantIdentifier tenantIdentifier, - @NotNull String email) - throws SQLException, StorageQueryException { - - 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"; - - List userInfos = execute(start, QUERY.toString(), pst -> { + public static List getPrimaryUserIdUsingEmail(Start start, + TenantIdentifier tenantIdentifier, String email) + throws StorageQueryException, SQLException { + String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id " + + "FROM " + getConfig(start).getThirdPartyUsersTable() + " AS tp" + + " JOIN " + getConfig(start).getUsersTable() + " AS all_users" + + " ON tp.app_id = all_users.app_id AND tp.user_id = all_users.user_id" + + " JOIN " + getConfig(start).getThirdPartyUserToTenantTable() + " AS tp_tenants" + + " ON tp_tenants.app_id = all_users.app_id AND tp_tenants.user_id = all_users.user_id" + + " WHERE tp.app_id = ? AND tp_tenants.tenant_id = ? AND tp.email = ?"; + + return execute(start, QUERY, 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)); + finalResult.add(result.getString("user_id")); } return finalResult; }); - return userInfoWithTenantIds(start, tenantIdentifier.toAppIdentifier(), userInfos).toArray(new UserInfo[0]); } - public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId) - throws SQLException, StorageQueryException { - UserInfoPartial userInfo = ThirdPartyQueries.getUserInfoUsingUserId(start, sqlCon, + public static List getPrimaryUserIdUsingEmail_Transaction(Start start, Connection con, + AppIdentifier appIdentifier, String email) + throws StorageQueryException, SQLException { + String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id " + + "FROM " + getConfig(start).getThirdPartyUsersTable() + " AS tp" + + " JOIN " + getConfig(start).getAppIdToUserIdTable() + " AS all_users" + + " ON tp.app_id = all_users.app_id AND tp.user_id = all_users.user_id" + + " WHERE tp.app_id = ? AND tp.email = ?"; + + return execute(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, email); + }, result -> { + List finalResult = new ArrayList<>(); + while (result.next()) { + finalResult.add(result.getString("user_id")); + } + return finalResult; + }); + } + + public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlCon, + TenantIdentifier tenantIdentifier, String userId) + throws SQLException, StorageQueryException, UnknownUserIdException { + UserInfoPartial userInfo = ThirdPartyQueries.getUserInfoUsingUserId_Transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), userId); + if (userInfo == null) { + throw new UnknownUserIdException(); + } + + GeneralQueries.AccountLinkingInfo accountLinkingInfo = GeneralQueries.getAccountLinkingInfo_Transaction(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"; + + "(app_id, tenant_id, user_id, primary_or_recipe_user_id, is_linked_or_is_a_primary_user, recipe_id, time_joined, primary_or_recipe_user_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); + pst.setString(4, accountLinkingInfo.primaryUserId); + pst.setBoolean(5, accountLinkingInfo.isLinked); + pst.setString(6, THIRD_PARTY.toString()); + pst.setLong(7, userInfo.timeJoined); + pst.setLong(8, userInfo.timeJoined); }); + + GeneralQueries.updateTimeJoinedForPrimaryUser_Transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), accountLinkingInfo.primaryUserId); } { // 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"; + + " VALUES(?, ?, ?, ?, ?)" + " ON CONFLICT ON CONSTRAINT " + + Utils.getConstraintName(Config.getConfig(start).getTableSchema(), getConfig(start).getThirdPartyUserToTenantTable(), null, "pkey") + + " DO NOTHING"; int numRows = update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, tenantIdentifier.getTenantId()); @@ -372,7 +500,8 @@ public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlC } } - public static boolean removeUserIdFromTenant_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId) + 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() @@ -390,56 +519,84 @@ public static boolean removeUserIdFromTenant_Transaction(Start start, Connection // automatically deleted from thirdparty_user_to_tenant because of foreign key constraint } - private static UserInfo userInfoWithTenantIds(Start start, AppIdentifier appIdentifier, UserInfoPartial userInfo) + private static UserInfoPartial fillUserInfoWithVerified_transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + UserInfoPartial userInfo) throws SQLException, StorageQueryException { if (userInfo == null) return null; - try (Connection con = ConnectionPool.getConnection(start)) { - return userInfoWithTenantIds_transaction(start, con, appIdentifier, Arrays.asList(userInfo)).get(0); - } + return fillUserInfoWithVerified_transaction(start, sqlCon, appIdentifier, List.of(userInfo)).get(0); } - private static List userInfoWithTenantIds(Start start, AppIdentifier appIdentifier, List userInfos) + private static List fillUserInfoWithVerified_transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + List userInfos) throws SQLException, StorageQueryException { - try (Connection con = ConnectionPool.getConnection(start)) { - return userInfoWithTenantIds_transaction(start, con, appIdentifier, userInfos); + List userIdsAndEmails = new ArrayList<>(); + for (UserInfoPartial userInfo : userInfos) { + userIdsAndEmails.add(new EmailVerificationQueries.UserIdAndEmail(userInfo.id, userInfo.email)); + } + List userIdsThatAreVerified = EmailVerificationQueries.isEmailVerified_transaction(start, sqlCon, + appIdentifier, + userIdsAndEmails); + Set verifiedUserIdsSet = new HashSet<>(userIdsThatAreVerified); + for (UserInfoPartial userInfo : userInfos) { + if (verifiedUserIdsSet.contains(userInfo.id)) { + userInfo.verified = true; + } else { + userInfo.verified = false; + } } + return userInfos; } - private static UserInfo userInfoWithTenantIds_transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, UserInfoPartial userInfo) + private static UserInfoPartial fillUserInfoWithTenantIds_transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + UserInfoPartial userInfo) throws SQLException, StorageQueryException { if (userInfo == null) return null; - return userInfoWithTenantIds_transaction(start, sqlCon, appIdentifier, Arrays.asList(userInfo)).get(0); + return fillUserInfoWithTenantIds_transaction(start, sqlCon, appIdentifier, List.of(userInfo)).get(0); } - private static List userInfoWithTenantIds_transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, List userInfos) + private static List fillUserInfoWithTenantIds_transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + 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, appIdentifier, userIds); - List result = new ArrayList<>(); + Map> tenantIdsForUserIds = GeneralQueries.getTenantIdsForUserIds_transaction(start, sqlCon, + appIdentifier, + userIds); for (UserInfoPartial userInfo : userInfos) { - result.add(new UserInfo(userInfo.id, userInfo.email, userInfo.thirdParty, userInfo.timeJoined, - tenantIdsForUserIds.get(userInfo.id).toArray(new String[0]))); + userInfo.tenantIds = tenantIdsForUserIds.get(userInfo.id).toArray(new String[0]); } - - return result; + return userInfos; } private static class UserInfoPartial { public final String id; public final String email; - public final UserInfo.ThirdParty thirdParty; + public final LoginMethod.ThirdParty thirdParty; public final long timeJoined; + public String[] tenantIds; + public Boolean verified; + public Boolean isPrimary; - public UserInfoPartial(String id, String email, UserInfo.ThirdParty thirdParty, long timeJoined) { + public UserInfoPartial(String id, String email, LoginMethod.ThirdParty thirdParty, long timeJoined) { this.id = id.trim(); this.email = email; this.thirdParty = thirdParty; this.timeJoined = timeJoined; } + + public LoginMethod toLoginMethod() { + assert (tenantIds != null); + assert (verified != null); + return new LoginMethod(id, timeJoined, verified, email, + new LoginMethod.ThirdParty(thirdParty.id, thirdParty.userId), tenantIds); + } } private static class UserInfoRowMapper implements RowMapper { @@ -455,7 +612,7 @@ private static UserInfoRowMapper getInstance() { @Override 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"), + new LoginMethod.ThirdParty(result.getString("third_party_id"), result.getString("third_party_user_id")), result.getLong("time_joined")); } 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 cc600818..a32dccb7 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/UserIdMappingQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/UserIdMappingQueries.java @@ -25,6 +25,7 @@ import io.supertokens.storage.postgresql.utils.Utils; import javax.annotation.Nullable; +import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; @@ -216,6 +217,56 @@ public static boolean updateOrDeleteExternalUserIdInfoWithExternalUserId(Start s return rowUpdated > 0; } + public static UserIdMapping getuseraIdMappingWithSuperTokensUserId_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, String userId) + throws SQLException, StorageQueryException { + String QUERY = "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable() + + " WHERE app_id = ? AND supertokens_user_id = ?"; + return execute(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }, result -> { + if (result.next()) { + return UserIdMappingRowMapper.getInstance().mapOrThrow(result); + } + return null; + }); + } + + public static UserIdMapping getUserIdMappingWithExternalUserId_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, String userId) + throws SQLException, StorageQueryException { + String QUERY = "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable() + + " WHERE app_id = ? AND external_user_id = ?"; + + return execute(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }, result -> { + if (result.next()) { + return UserIdMappingRowMapper.getInstance().mapOrThrow(result); + } + return null; + }); + } + + public static UserIdMapping[] getUserIdMappingWithEitherSuperTokensUserIdOrExternalUserId_Transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, String userId) + throws SQLException, StorageQueryException { + String QUERY = "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable() + + " WHERE app_id = ? AND (supertokens_user_id = ? OR external_user_id = ?)"; + + return execute(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + pst.setString(3, userId); + }, result -> { + ArrayList userIdMappingArray = new ArrayList<>(); + while (result.next()) { + userIdMappingArray.add(UserIdMappingRowMapper.getInstance().mapOrThrow(result)); + } + return userIdMappingArray.toArray(UserIdMapping[]::new); + }); + } + private static class UserIdMappingRowMapper implements RowMapper { private static final UserIdMappingRowMapper INSTANCE = new UserIdMappingRowMapper(); 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 d645bad1..1d2b6231 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/UserMetadataQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/UserMetadataQueries.java @@ -18,7 +18,6 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; - import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.storage.postgresql.Start; @@ -46,7 +45,7 @@ public static String getQueryToCreateUserMetadataTable(Start start) { + " 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" + + " REFERENCES " + Config.getConfig(start).getAppsTable() + " (app_id) ON DELETE CASCADE" + ");"; // @formatter:on } @@ -56,7 +55,8 @@ public static String getQueryToCreateAppIdIndexForUserMetadataTable(Start start) + Config.getConfig(start).getUserMetadataTable() + "(app_id);"; } - public static int deleteUserMetadata(Start start, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { + 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 = ?"; @@ -66,7 +66,20 @@ public static int deleteUserMetadata(Start start, AppIdentifier appIdentifier, S }); } - public static int setUserMetadata_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String userId, JsonObject metadata) + public static int deleteUserMetadata_Transaction(Connection sqlCon, Start start, AppIdentifier appIdentifier, + String userId) + throws SQLException, StorageQueryException { + String QUERY = "DELETE FROM " + getConfig(start).getUserMetadataTable() + + " WHERE app_id = ? AND user_id = ?"; + + return update(sqlCon, QUERY.toString(), pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); + } + + 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() @@ -97,7 +110,8 @@ public static JsonObject getUserMetadata_Transaction(Start start, Connection con }); } - public static JsonObject getUserMetadata(Start start, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { + 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 -> { 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 3069faa6..549cac86 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/UserRolesQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/UserRolesQueries.java @@ -19,7 +19,6 @@ import io.supertokens.pluginInterface.exceptions.StorageQueryException; 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; @@ -45,7 +44,7 @@ public static String getQueryToCreateRolesTable(Start start) { + " 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" + + " REFERENCES " + Config.getConfig(start).getAppsTable() + " (app_id) ON DELETE CASCADE" + ");"; // @formatter:on } @@ -103,11 +102,13 @@ public static String getQueryToCreateUserRolesTable(Start start) { } 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);"; + 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);"; + 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) { @@ -116,7 +117,7 @@ public static String getQueryToCreateUserRolesRoleIndex(Start start) { } public static boolean createNewRoleOrDoNothingIfExists_Transaction(Start start, Connection con, - AppIdentifier appIdentifier, String role) + AppIdentifier appIdentifier, String role) throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + getConfig(start).getRolesTable() + "(app_id, role) VALUES (?, ?) ON CONFLICT DO NOTHING;"; @@ -129,7 +130,8 @@ public static boolean createNewRoleOrDoNothingIfExists_Transaction(Start start, public static void addPermissionToRoleOrDoNothingIfExists_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String role, - String permission) throws SQLException, StorageQueryException { + String permission) + throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + getConfig(start).getUserRolesPermissionsTable() + " (app_id, role, permission) VALUES(?, ?, ?) ON CONFLICT DO NOTHING"; @@ -140,7 +142,8 @@ public static void addPermissionToRoleOrDoNothingIfExists_Transaction(Start star }); } - public static boolean deleteRole(Start start, AppIdentifier appIdentifier, String role) throws SQLException, StorageQueryException { + 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 -> { @@ -149,7 +152,8 @@ public static boolean deleteRole(Start start, AppIdentifier appIdentifier, Strin }) == 1; } - public static boolean doesRoleExist(Start start, AppIdentifier appIdentifier, String role) throws SQLException, StorageQueryException { + 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 -> { @@ -158,7 +162,8 @@ public static boolean doesRoleExist(Start start, AppIdentifier appIdentifier, St }, ResultSet::next); } - public static String[] getPermissionsForRole(Start start, AppIdentifier appIdentifier, 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 app_id = ? AND role = ?;"; return execute(start, QUERY, pst -> { @@ -173,7 +178,8 @@ public static String[] getPermissionsForRole(Start start, AppIdentifier appIdent }); } - public static String[] getRoles(Start start, AppIdentifier appIdentifier) throws SQLException, StorageQueryException { + 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<>(); @@ -247,7 +253,8 @@ public static boolean deleteRoleForUser_Transaction(Start start, Connection con, return rowUpdatedCount > 0; } - public static boolean doesRoleExist_transaction(Start start, Connection con, AppIdentifier appIdentifier, 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 app_id = ? AND role = ? FOR UPDATE"; @@ -257,7 +264,8 @@ public static boolean doesRoleExist_transaction(Start start, Connection con, App }, ResultSet::next); } - public static String[] getUsersForRole(Start start, TenantIdentifier tenantIdentifier, String role) throws SQLException, StorageQueryException { + 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 -> { @@ -275,7 +283,8 @@ public static String[] getUsersForRole(Start start, TenantIdentifier tenantIdent public static boolean deletePermissionForRole_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String role, - String permission) throws SQLException, StorageQueryException { + String permission) + throws SQLException, StorageQueryException { String QUERY = "DELETE FROM " + getConfig(start).getUserRolesPermissionsTable() + " WHERE app_id = ? AND role = ? AND permission = ? "; @@ -323,7 +332,8 @@ public static String[] getRolesThatHavePermission(Start start, AppIdentifier app }); } - public static int deleteAllRolesForUser(Start start, TenantIdentifier tenantIdentifier, String userId) throws SQLException, StorageQueryException { + 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 -> { @@ -333,10 +343,12 @@ public static int deleteAllRolesForUser(Start start, TenantIdentifier tenantIden }); } - public static int deleteAllRolesForUser(Start start, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { + public static int deleteAllRolesForUser_Transaction(Connection con, 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 -> { + return update(con, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); pst.setString(2, userId); }); 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 7db2d0a5..91a58735 100644 --- a/src/main/java/io/supertokens/storage/postgresql/utils/Utils.java +++ b/src/main/java/io/supertokens/storage/postgresql/utils/Utils.java @@ -42,4 +42,15 @@ public static String getConstraintName(String schema, String prefixedTableName, constraintName.append('_').append(typeSuffix); return constraintName.toString(); } + + public static String generateCommaSeperatedQuestionMarks(int size) { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < size; i++) { + builder.append("?"); + if (i != size - 1) { + builder.append(","); + } + } + return builder.toString(); + } } diff --git a/src/test/java/io/supertokens/storage/postgresql/test/AccountLinkingTests.java b/src/test/java/io/supertokens/storage/postgresql/test/AccountLinkingTests.java new file mode 100644 index 00000000..4f26a52c --- /dev/null +++ b/src/test/java/io/supertokens/storage/postgresql/test/AccountLinkingTests.java @@ -0,0 +1,154 @@ +/* + * 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.authRecipe.AuthRecipe; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.multitenancy.Multitenancy; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.multitenancy.*; +import io.supertokens.storage.postgresql.test.httpRequest.HttpRequestForTesting; +import io.supertokens.storage.postgresql.test.httpRequest.HttpResponseException; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.webserver.WebserverAPI; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertNotNull; + +public class AccountLinkingTests { + + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + @Test + public void canLinkFailsIfTryingToLinkUsersAcrossDifferentStorageLayers() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + 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)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + if (StorageLayer.isInMemDb(process.getProcess())) { + return; + } + + + JsonObject coreConfig = new JsonObject(); + StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess()) + .modifyConfigToAddANewUserPoolForTesting(coreConfig, 2); + + TenantIdentifier tenantIdentifier = new TenantIdentifier(null, null, "t1"); + Multitenancy.addNewOrUpdateAppOrTenant( + process.getProcess(), + new TenantIdentifier(null, null, null), + new TenantConfig( + tenantIdentifier, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + coreConfig + ) + ); + + AuthRecipeUserInfo user1 = EmailPassword.signUp(process.getProcess(), "test@example.com", "abcd1234"); + + AuthRecipe.createPrimaryUser(process.main, user1.getSupertokensUserId()); + + AuthRecipeUserInfo user2 = EmailPassword.signUp( + tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, process.main)), + process.getProcess(), "test2@example.com", "abcd1234"); + + try { + Map params = new HashMap<>(); + params.put("recipeUserId", user2.getSupertokensUserId()); + params.put("primaryUserId", user1.getSupertokensUserId()); + + HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/link/check", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assert (false); + } catch (HttpResponseException e) { + assert (e.statusCode == 400); + assert (e.getMessage() + .equals("Http error. Status Code: 400. Message: Cannot link users that are parts of different " + + "databases. Different pool IDs: |localhost|5432|supertokens|public AND " + + "|localhost|5432|st2|public")); + } + + + coreConfig = new JsonObject(); + coreConfig.addProperty("postgresql_connection_pool_size", 11); + + tenantIdentifier = new TenantIdentifier(null, null, "t2"); + Multitenancy.addNewOrUpdateAppOrTenant( + process.getProcess(), + new TenantIdentifier(null, null, null), + new TenantConfig( + tenantIdentifier, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + coreConfig + ) + ); + + AuthRecipeUserInfo user3 = EmailPassword.signUp( + tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, process.main)), + process.getProcess(), "test2@example.com", "abcd1234"); + + Map params = new HashMap<>(); + params.put("recipeUserId", user3.getSupertokensUserId()); + params.put("primaryUserId", user1.getSupertokensUserId()); + + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/link/check", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assert (response.get("status").getAsString().equals("OK")); + assert (!response.get("accountsAlreadyLinked").getAsBoolean()); + + 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 fb5130a8..7fd988ac 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java @@ -87,7 +87,7 @@ public void thirdPartySignupExceptions() throws Exception { String thirdPartyUserId = "tp_userId"; String userEmail = "useremail@asdf.fdas"; - var tp = new io.supertokens.pluginInterface.thirdparty.UserInfo.ThirdParty(tpId, thirdPartyUserId); + var tp = new io.supertokens.pluginInterface.authRecipe.LoginMethod.ThirdParty(tpId, thirdPartyUserId); storage.signUp(new TenantIdentifier(null, null, null), userId, userEmail, tp, System.currentTimeMillis()); try { @@ -128,15 +128,18 @@ public void emailPasswordSignupExceptions() throws Exception { String pwHash = "fakehash"; String userEmail = "useremail@asdf.fdas"; - storage.signUp(new TenantIdentifier(null, null, null), userId, userEmail, pwHash, System.currentTimeMillis()); + storage.signUp(new TenantIdentifier(null, null, null), userId, userEmail, pwHash, + System.currentTimeMillis()); try { - storage.signUp(new TenantIdentifier(null, null, null), userId, userEmail, pwHash, System.currentTimeMillis()); + storage.signUp(new TenantIdentifier(null, null, null), userId, userEmail, pwHash, + System.currentTimeMillis()); throw new Exception("This should throw"); } catch (DuplicateUserIdException ex) { // expected } try { - storage.signUp(new TenantIdentifier(null, null, null), userId2, userEmail, pwHash, System.currentTimeMillis()); + storage.signUp(new TenantIdentifier(null, null, null), userId2, userEmail, pwHash, + System.currentTimeMillis()); throw new Exception("This should throw"); } catch (DuplicateEmailException ex) { // expected @@ -171,8 +174,10 @@ public void updateUsersEmail_TransactionExceptions() String userEmail2 = "useremail2@asdf.fdas"; String userEmail3 = "useremail3@asdf.fdas"; - 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.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); @@ -211,7 +216,8 @@ public void updateIsEmailVerified_TransactionExceptions() TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - EmailVerificationSQLStorage storage = (EmailVerificationSQLStorage) StorageLayer.getStorage(process.getProcess()); + EmailVerificationSQLStorage storage = (EmailVerificationSQLStorage) StorageLayer.getStorage( + process.getProcess()); String userId = "userId"; String userEmail = "useremail@asdf.fdas"; @@ -219,8 +225,9 @@ public void updateIsEmailVerified_TransactionExceptions() storage.startTransaction(conn -> { try { storage.updateIsEmailVerified_Transaction(new AppIdentifier(null, null), conn, userId, userEmail, - true); - storage.updateIsEmailVerified_Transaction(new AppIdentifier(null, null), conn, userId, userEmail, true); + true); + storage.updateIsEmailVerified_Transaction(new AppIdentifier(null, null), conn, userId, userEmail, + true); } catch (TenantOrAppNotFoundException e) { throw new RuntimeException(e); } @@ -280,11 +287,12 @@ public void addPasswordResetTokenExceptions() throws Exception { String pwHash = "fakehash"; String userEmail = "useremail@asdf.fdas"; - var info = new PasswordResetTokenInfo(userId, tokenHash, System.currentTimeMillis() + 10000); + var info = new PasswordResetTokenInfo(userId, tokenHash, System.currentTimeMillis() + 10000, userEmail); try { storage.addPasswordResetToken(new AppIdentifier(null, null), info); } catch (UnknownUserIdException ex) { - storage.signUp(new TenantIdentifier(null, null, null), userId, userEmail, pwHash, System.currentTimeMillis()); + storage.signUp(new TenantIdentifier(null, null, null), userId, userEmail, pwHash, + System.currentTimeMillis()); } storage.addPasswordResetToken(new AppIdentifier(null, null), info); try { @@ -306,7 +314,8 @@ public void addEmailVerificationTokenExceptions() throws Exception { TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - EmailVerificationSQLStorage storage = (EmailVerificationSQLStorage) StorageLayer.getStorage(process.getProcess()); + EmailVerificationSQLStorage storage = (EmailVerificationSQLStorage) StorageLayer.getStorage( + process.getProcess()); String userId = "userId"; String tokenHash = "fakehash"; @@ -340,16 +349,19 @@ public void verifyEmailExceptions() throws Exception { String pwHash = "fakehash"; String userEmail = "useremail@asdf.fdas"; - storage.signUp(new TenantIdentifier(null, null, null), userId, userEmail, pwHash, System.currentTimeMillis()); + storage.signUp(new TenantIdentifier(null, null, null), userId, userEmail, pwHash, + System.currentTimeMillis()); try { - storage.signUp(new TenantIdentifier(null, null, null), userId, userEmail, pwHash, System.currentTimeMillis()); + storage.signUp(new TenantIdentifier(null, null, null), userId, userEmail, pwHash, + System.currentTimeMillis()); throw new Exception("This should throw"); } catch (DuplicateUserIdException ex) { // expected } try { - storage.signUp(new TenantIdentifier(null, null, null), userId2, userEmail, pwHash, System.currentTimeMillis()); + storage.signUp(new TenantIdentifier(null, null, null), userId2, userEmail, pwHash, + System.currentTimeMillis()); throw new Exception("This should throw"); } catch (DuplicateEmailException ex) { // expected 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 e8c7493f..5a1d7a1f 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,7 +25,7 @@ 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.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.exceptions.InvalidConfigException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.*; @@ -91,7 +91,7 @@ public void testUsersWorkAfterUserPoolIdChanges() throws Exception { String userPoolId = tenantIdentifierWithStorage.getStorage().getUserPoolId(); - UserInfo userInfo = EmailPassword.signUp( + AuthRecipeUserInfo userInfo = EmailPassword.signUp( tenantIdentifierWithStorage, process.getProcess(), "user@example.com", "password"); coreConfig.addProperty("postgresql_host", "127.0.0.1"); @@ -108,7 +108,8 @@ public void testUsersWorkAfterUserPoolIdChanges() throws Exception { String userPoolId2 = tenantIdentifierWithStorage.getStorage().getUserPoolId(); assertNotEquals(userPoolId, userPoolId2); - UserInfo user2 = EmailPassword.signIn(tenantIdentifierWithStorage, process.getProcess(), "user@example.com", "password"); + AuthRecipeUserInfo user2 = EmailPassword.signIn(tenantIdentifierWithStorage, process.getProcess(), + "user@example.com", "password"); assertEquals(userInfo, user2); @@ -134,7 +135,7 @@ public void testUsersWorkAfterUserPoolIdChangesAndServerRestart() throws Excepti String userPoolId = tenantIdentifierWithStorage.getStorage().getUserPoolId(); - UserInfo userInfo = EmailPassword.signUp( + AuthRecipeUserInfo userInfo = EmailPassword.signUp( tenantIdentifierWithStorage, process.getProcess(), "user@example.com", "password"); coreConfig.addProperty("postgresql_host", "127.0.0.1"); @@ -157,7 +158,9 @@ public void testUsersWorkAfterUserPoolIdChangesAndServerRestart() throws Excepti String userPoolId2 = tenantIdentifierWithStorage.getStorage().getUserPoolId(); assertNotEquals(userPoolId, userPoolId2); - UserInfo user2 = EmailPassword.signIn(tenantIdentifierWithStorage, process.getProcess(), "user@example.com", "password"); + AuthRecipeUserInfo user2 = EmailPassword.signIn(tenantIdentifierWithStorage, process.getProcess(), + "user@example.com", + "password"); assertEquals(userInfo, user2); } From afe34d198ce0609623b43bc6b6961ca9fd7efc30 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Wed, 20 Sep 2023 12:06:08 +0530 Subject: [PATCH 089/148] adding dev-v5.0.0 tag to this commit to ensure building --- jar/postgresql-plugin-5.0.0.jar | Bin 206459 -> 206623 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/jar/postgresql-plugin-5.0.0.jar b/jar/postgresql-plugin-5.0.0.jar index d1eb94f58d9af000058dd880b067b18a0c4bf380..b41802c8580979663607b205fc4aaa2e70a3ff25 100644 GIT binary patch delta 66912 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

L%Tl|JMg8Bzrp8ia=#P8IK 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 Qur(bJ11{vx;~47y0M^qfb^rhX delta 66823 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�Z%-=!{g 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 pN7Di+o^H@M_+|!(hz{}(k^t%7SKfPL0C Date: Wed, 20 Sep 2023 18:41:35 +0530 Subject: [PATCH 090/148] fix: deadlock and storage tests (#160) * fix: deadlock tests * fix: storage layer test --- .../storage/postgresql/test/DeadlockTest.java | 93 +++++++++++++++++++ .../postgresql/test/StorageLayerTest.java | 58 +++++++++++- 2 files changed, 150 insertions(+), 1 deletion(-) 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 15d8016b..3dd0241f 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/DeadlockTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/DeadlockTest.java @@ -18,9 +18,14 @@ package io.supertokens.storage.postgresql.test; import io.supertokens.ProcessState; +import io.supertokens.authRecipe.AuthRecipe; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; import io.supertokens.passwordless.Passwordless; import io.supertokens.pluginInterface.KeyValueInfo; import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; @@ -599,6 +604,94 @@ public void testConcurrentDeleteAndInsert() throws Exception { assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } + @Test + public void testLinkAccountsInParallel() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + 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)); + + ExecutorService es = Executors.newFixedThreadPool(1000); + + AtomicBoolean pass = new AtomicBoolean(true); + + AuthRecipeUserInfo user1 = EmailPassword.signUp(process.getProcess(), "test1@example.com", "password"); + AuthRecipeUserInfo user2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", "password"); + + AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId()); + + for (int i = 0; i < 3000; i++) { + es.execute(() -> { + try { + AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), user1.getSupertokensUserId()); + AuthRecipe.unlinkAccounts(process.getProcess(), user2.getSupertokensUserId()); + } catch (Exception e) { + if (e.getMessage().toLowerCase().contains("the transaction might succeed if retried")) { + pass.set(false); + } + } + }); + } + + es.shutdown(); + es.awaitTermination(2, TimeUnit.MINUTES); + + assert (pass.get()); + assertNull(process + .checkOrWaitForEventInPlugin(io.supertokens.storage.postgresql.ProcessState.PROCESS_STATE.DEADLOCK_NOT_RESOLVED)); + assertNotNull(process + .checkOrWaitForEventInPlugin(io.supertokens.storage.postgresql.ProcessState.PROCESS_STATE.DEADLOCK_FOUND)); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testCreatePrimaryInParallel() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + 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)); + + ExecutorService es = Executors.newFixedThreadPool(1000); + + AtomicBoolean pass = new AtomicBoolean(true); + + AuthRecipeUserInfo user1 = EmailPassword.signUp(process.getProcess(), "test1@example.com", "password"); + + for (int i = 0; i < 3000; i++) { + es.execute(() -> { + try { + AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId()); + AuthRecipe.unlinkAccounts(process.getProcess(), user1.getSupertokensUserId()); + } catch (Exception e) { + if (e.getMessage().toLowerCase().contains("the transaction might succeed if retried")) { + pass.set(false); + } + } + }); + } + + es.shutdown(); + es.awaitTermination(2, TimeUnit.MINUTES); + + assert (pass.get()); + assertNull(process + .checkOrWaitForEventInPlugin(io.supertokens.storage.postgresql.ProcessState.PROCESS_STATE.DEADLOCK_NOT_RESOLVED)); + assertNotNull(process + .checkOrWaitForEventInPlugin(io.supertokens.storage.postgresql.ProcessState.PROCESS_STATE.DEADLOCK_FOUND)); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } } /* 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 7f39a6ca..b48324bb 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/StorageLayerTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/StorageLayerTest.java @@ -1,7 +1,14 @@ package io.supertokens.storage.postgresql.test; import io.supertokens.ProcessState; +import io.supertokens.authRecipe.AuthRecipe; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.passwordless.Passwordless; import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeStorage; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; @@ -14,13 +21,14 @@ import io.supertokens.pluginInterface.totp.sqlStorage.TOTPSQLStorage; import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.thirdparty.ThirdParty; 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; +import static org.junit.Assert.*; public class StorageLayerTest { @@ -94,4 +102,52 @@ public void totpCodeLengthTest() throws Exception { insertUsedCodeUtil(storage, code); } + @Test + public void testLinkedAccountUser() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + 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)); + + AuthRecipeUserInfo user1 = EmailPassword.signUp(process.getProcess(), "test1@example.com", "password"); + Thread.sleep(50); + AuthRecipeUserInfo user2 = ThirdParty.signInUp(process.getProcess(), "google", "googleid", "test2@example.com").user; + Thread.sleep(50); + Passwordless.CreateCodeResponse code1 = Passwordless.createCode(process.getProcess(), "test3@example.com", null, null, null); + AuthRecipeUserInfo user3 = Passwordless.consumeCode(process.getProcess(), code1.deviceId, code1.deviceIdHash, code1.userInputCode, null).user; + Thread.sleep(50); + Passwordless.CreateCodeResponse code2 = Passwordless.createCode(process.getProcess(), null, "+919876543210", null, null); + AuthRecipeUserInfo user4 = Passwordless.consumeCode(process.getProcess(), code2.deviceId, code2.deviceIdHash, code2.userInputCode, null).user; + + AuthRecipe.createPrimaryUser(process.getProcess(), user3.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), user1.getSupertokensUserId(), user3.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), user3.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), user4.getSupertokensUserId(), user3.getSupertokensUserId()); + + String[] userIds = new String[]{ + user1.getSupertokensUserId(), + user2.getSupertokensUserId(), + user3.getSupertokensUserId(), + user4.getSupertokensUserId() + }; + + for (String userId : userIds){ + AuthRecipeUserInfo primaryUser = ((AuthRecipeStorage) StorageLayer.getStorage(process.getProcess())).getPrimaryUserById( + new AppIdentifier(null, null), userId); + assertEquals(user3.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + assertEquals(4, primaryUser.loginMethods.length); + assertTrue(primaryUser.loginMethods[0].timeJoined < primaryUser.loginMethods[1].timeJoined); + assertTrue(primaryUser.loginMethods[1].timeJoined < primaryUser.loginMethods[2].timeJoined); + assertTrue(primaryUser.loginMethods[2].timeJoined < primaryUser.loginMethods[3].timeJoined); + assertEquals(primaryUser.timeJoined, primaryUser.loginMethods[0].timeJoined); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + } From dc63818390b24969c35f944d9b2550e30f852656 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Wed, 20 Sep 2023 18:46:41 +0530 Subject: [PATCH 091/148] adding dev-v5.0.0 tag to this commit to ensure building --- jar/postgresql-plugin-5.0.0.jar | Bin 206623 -> 206627 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/jar/postgresql-plugin-5.0.0.jar b/jar/postgresql-plugin-5.0.0.jar index b41802c8580979663607b205fc4aaa2e70a3ff25..efa78f98ffd7e28709945ee5f5930d9f6c9b2616 100644 GIT binary patch delta 24267 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 b-eE972r6snvK+po0=tC(eex)HCy4(8(tMl@ delta 24058 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 Date: Thu, 28 Sep 2023 16:47:56 +0530 Subject: [PATCH 092/148] 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 093/148] 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 094/148] 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 ab0db7dc6fd6c835bb903d1b8f44ef47baf25456 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Thu, 12 Oct 2023 19:19:56 +0530 Subject: [PATCH 095/148] fix: query fix (#165) --- CHANGELOG.md | 4 ++++ build.gradle | 2 +- .../storage/postgresql/queries/GeneralQueries.java | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 496fcff2..69ef7e9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [5.0.1] - 2023-10-12 + +- Fixes user info from primary user id query + ## [5.0.0] - 2023-09-19 ### Changes diff --git a/build.gradle b/build.gradle index a3e8c53f..736fe41b 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'java-library' } -version = "5.0.0" +version = "5.0.1" repositories { mavenCentral() 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 81583518..976c3337 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java @@ -1368,7 +1368,7 @@ private static List getPrimaryUserInfoForUserIds(Start start " WHERE au.primary_or_recipe_user_id IN (SELECT primary_or_recipe_user_id FROM " + getConfig(start).getAppIdToUserIdTable() + " WHERE (user_id IN (" + Utils.generateCommaSeperatedQuestionMarks(userIds.size()) + - ") OR au.primary_or_recipe_user_id IN (" + + ") OR primary_or_recipe_user_id IN (" + Utils.generateCommaSeperatedQuestionMarks(userIds.size()) + ")) AND app_id = ?) AND au.app_id = ?"; @@ -1460,7 +1460,7 @@ private static List getPrimaryUserInfoForUserIds_Transaction " WHERE au.primary_or_recipe_user_id IN (SELECT primary_or_recipe_user_id FROM " + getConfig(start).getAppIdToUserIdTable() + " WHERE (user_id IN (" + Utils.generateCommaSeperatedQuestionMarks(userIds.size()) + - ") OR au.primary_or_recipe_user_id IN (" + + ") OR primary_or_recipe_user_id IN (" + Utils.generateCommaSeperatedQuestionMarks(userIds.size()) + ")) AND app_id = ?) AND au.app_id = ?"; From f8ba21c49b3304f0488d7714c7b707a512244003 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Thu, 12 Oct 2023 19:20:35 +0530 Subject: [PATCH 096/148] adding dev-v5.0.1 tag to this commit to ensure building --- ...-5.0.0.jar => postgresql-plugin-5.0.1.jar} | Bin 206627 -> 206627 bytes 1 file changed, 0 insertions(+), 0 deletions(-) rename jar/{postgresql-plugin-5.0.0.jar => postgresql-plugin-5.0.1.jar} (89%) diff --git a/jar/postgresql-plugin-5.0.0.jar b/jar/postgresql-plugin-5.0.1.jar similarity index 89% rename from jar/postgresql-plugin-5.0.0.jar rename to jar/postgresql-plugin-5.0.1.jar index efa78f98ffd7e28709945ee5f5930d9f6c9b2616..26787ab9bc518862e93f0edcd3d0de8cb9f0a7a3 100644 GIT binary patch delta 12814 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

exFYvIvQSS#SJI)rR5Px@4LVJY2 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 G`+oq#ar#LB From 9435be3bf52ba2327d884cf3518fb3b21f37ab95 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Thu, 12 Oct 2023 19:40:20 +0530 Subject: [PATCH 097/148] Query fix (#166) * fix: query fix * fix: passwordless --- CHANGELOG.md | 1 + .../storage/postgresql/queries/PasswordlessQueries.java | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69ef7e9d..a01447b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [5.0.1] - 2023-10-12 - Fixes user info from primary user id query +- Fixes `deviceIdHash` issue ## [5.0.0] - 2023-09-19 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 31858944..8f8df3d6 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java @@ -1112,7 +1112,7 @@ private static PasswordlessDeviceRowMapper getInstance() { @Override public PasswordlessDevice map(ResultSet result) throws Exception { - return new PasswordlessDevice(result.getString("device_id_hash"), result.getString("email"), + return new PasswordlessDevice(result.getString("device_id_hash").trim(), result.getString("email"), result.getString("phone_number"), result.getString("link_code_salt"), result.getInt("failed_attempts")); } @@ -1130,7 +1130,7 @@ private static PasswordlessCodeRowMapper getInstance() { @Override public PasswordlessCode map(ResultSet result) throws Exception { - return new PasswordlessCode(result.getString("code_id"), result.getString("device_id_hash"), + return new PasswordlessCode(result.getString("code_id"), result.getString("device_id_hash").trim(), result.getString("link_code_hash"), result.getLong("created_at")); } } From 45f662c2a2b852a58ca3b47a6099cd6ebf3762db Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Thu, 12 Oct 2023 19:40:49 +0530 Subject: [PATCH 098/148] adding dev-v5.0.1 tag to this commit to ensure building --- jar/postgresql-plugin-5.0.1.jar | Bin 206627 -> 206684 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/jar/postgresql-plugin-5.0.1.jar b/jar/postgresql-plugin-5.0.1.jar index 26787ab9bc518862e93f0edcd3d0de8cb9f0a7a3..c06e9367c873a94d471fb9876b6c5a66c68cf24b 100644 GIT binary patch delta 4080 zcmZWs3p`X?7oT&s$HaIJW-u6U1`R5&@}>ymt%ORF2pNehYETg&uR4T8O1`chbR$a9 z<0^TDFDezOltK@Bxb&Sp^Siq4H^1MU|Jv)n*4lgRefHk#oa!V!?Ie-hoB*E?fj}e@ zD5be1O%#8wa8j2kbzVone<*z5KY;&3cQOItm$^)7?rA*~fA+1wo$`edE4)$Xg8a^h zBUa;yF3)EQkVRpn>d~30chUsHgi(GnQwvuM)^P-cWT5?Fn$Qy5y$(|shJJOG+Fkq* zcQZF2Fl>*$1El2*jNGBxGo^!1l!Av`Khmq$CP5(hSYgVWQLX%>B4XXfwD%Iw$hpW9 zK&SGO25wBh@?8MxyDQDKA(A?Kxcu{;2VAV_pGKj5<-j!Tv#$7YiWIa{nvA94Z447e zSUA?d5k^9wVZECuBFP7Zt&&K&2owbAh$HNy-a{3MhC%D^Y9Ni!220UFyrE)H2YJO0 zg_{P*P9Z25tU{!@8V75{SQu)Oosj|(6ezw(v;d5EcoXtK7HWLs5o>PXl0?K&0&3o5 zA>Y-Y%}6a$%I!S)3ewNDS>K3k1rl)TAKG}9=-{iz79#pl_nVIU=43}sKTfrrJ(XGa zsP5W|#9IYT#(b)*u#r)<7GJjNi#y9{Jm{bz&Ls8iRzL8)?` zO7r&9i3$h2T^+tR1k3yHY|`9#HM>xFqvePQy46AEjFYRz8O(YAZim5~W4i_5w?@MA%Lv5SO(l-jvvXhj_66eUAQgZFv z>O6Xw=Zf;RjF>sOTSejQtl47*mY4er?kdxadP0VYKa9VOQ*x|pJG+ZNM46=rkpe0@ z1DNTEz`09O-zr9o#}BJGkEnSMU!6akLH4q@k9JizXuJ5v^!wqkgF5Q(MAUbOhUk{q zA3L*uE&T*_ChmUO-5r$F>F`(7435I8`A!ciqbgLq%a5AuvfuZONNP0cMsiR^R6$;&>!1e5`)D6+_oUr3E9_E$X z3LCnjcK8idG_9Z7E5X`xxe8meuh)Xq?WS=vD!18UPO3NPnZ3-OTlWmto^#K0_DfsW z;^W}>!fi>_|N;ozF$Py{oo#8?BV; zXE(XLs3sD}=2J%hVk}Kex>}o_>T4FODWsp=lv{H9x&v$2+D@~B_*N`;e#x(658rrB zEl8Op+<5PM5eIm})Ef;T`?))?=?Rhx&$W8iC*%Z|nwUXu0q!Zt@%5T;$B$Z7I>zQl z_3I~pATdJFHkcpXV+w^~3-rq|E&) zLT&keXWph{9I-ii_FJU2gms2=fuTvJQlWZ)6J1T+v88O-)w)!oNA}OId96R2Lc@jo z0%>1t6XLp_nI}vH8C&k#_g#R#|Bc+1?e67cCQ18A3Qmbx@4Vh$zU`M% zlv9s+O@^avHLvYv8_Jhmd%hRKdX`Tx@L<`o=L^X+3X;b&K%M%?S2>G8(Rc9{MLic1$J)4 zRqfga{;XHyZ_lR`JsWbPm69UtFKsLORPfz?f*Cf}sx4i$yQF0>B6!y2d`>{1$?>rJ z11_0YUg>tFJyZ{(QK-v%$*aWg#4EOKUiz-#K)Vz@y|XEAdi}C{WI2y9@182vtaG>h z zUrtQ*Vc2wj1QUgP&8!cS^IhlphvJzJ#Qt!tD{wE%Y+hk+deQ&Sbq3DCoSLc|S87k) zIM;RW%6vAx@x5~6It)mvM@{EbPg{H0JAmh1ebn6WdlnaRUObB&|&E*JhnY{uA{ zqf0vk+6>cYB2unbS(CnwZQ92XJSkPKg?1xBQRt zgC^^`Pn$HX_IYQvF*q~kUJpHWtS`+&VsMSZt;!edN-IY0e0uYjI5o@RJ@%#77=^}s z#fKbrconqgZg{rGM&;zR&d9FfhG~g+vkE=T?kk$Z$SJDu2hYqjqqLAfnQDXWC22Fl zzcw(wy?j{cIK1iWZQaoA@WO=U${MJc@kAB47qIkZ)CGvZN`tvHC56C8AXH&d@u+rv zVGydy-PqSdPynj|tThx}4Sj{e&}G~k-!c+)hKj66G=$sD%uY0xD@3xZm^9D4NQ32N?P+6z}Wk zOP~+OB8>nJ#A676RTBg4ivEj|WE!Y2JL)NP`4u$B^OizvIX~ycdcu%#^~TnkawkJl}Q5|f*Hsw;NkYOxJrOGOY!Fal!}dNz9E7jNwqz>A0tH z81!UdJ#;{U%6Cv19?1)rqyk8oi83H@$Fv*V-1rlZ)r=4b&I>UKgv#W`f7*9MqKcx6 zlg4f;!&!;?M^sT2uoU)Zp@K8T>;;Caya<&)qZgDUY+eOu!ECiuab{Z#kBNvuRj^D| z-Yaz+GS6dq%u`h$%Y7f5`z0z6yeeY;-#q5U^Gq)sMah`68t-M_tcC}9pU7iAC!+L4 ziQxTrLxUL(&tQ)8ujPerp#S|g2AiKiI3z|Om@k^8r9+3x delta 3961 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 Date: Tue, 17 Oct 2023 13:56:27 +0530 Subject: [PATCH 099/148] 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 100/148] 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 101/148] 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 ddefb534f004078b8d54bcf9af19fdf61e97c8e9 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Wed, 1 Nov 2023 12:35:13 +0530 Subject: [PATCH 102/148] fix: email verified in login methods (#171) * fix: email verified in login methods * fix: version update * fix: pr comments --- CHANGELOG.md | 4 + build.gradle | 2 +- .../queries/EmailVerificationQueries.java | 87 ++++++++++++++----- .../queries/UserIdMappingQueries.java | 36 +++++++- 4 files changed, 106 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a01447b6..eeba6da2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [5.0.2] - 2023-11-01 + +- Fixes `verified` in `loginMethods` for users with userId mapping + ## [5.0.1] - 2023-10-12 - Fixes user info from primary user id query diff --git a/build.gradle b/build.gradle index 736fe41b..e682b205 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'java-library' } -version = "5.0.1" +version = "5.0.2" repositories { mavenCentral() 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 86b9359d..3547f084 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/EmailVerificationQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/EmailVerificationQueries.java @@ -259,26 +259,50 @@ public static List isEmailVerified_transaction(Start start, Connection s return new ArrayList<>(); } List emails = new ArrayList<>(); - List userIds = new ArrayList<>(); - Map userIdToEmailMap = new HashMap<>(); + List supertokensUserIds = new ArrayList<>(); for (UserIdAndEmail ue : userIdAndEmail) { emails.add(ue.email); - userIds.add(ue.userId); + supertokensUserIds.add(ue.userId); } + + // 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_Transaction(start, + sqlCon, supertokensUserIds); + HashMap externalUserIdToSupertokensUserIdMap = new HashMap<>(); + + List supertokensOrExternalUserIdsToQuery = new ArrayList<>(); + for (String userId : supertokensUserIds) { + if (supertokensUserIdToExternalUserIdMap.containsKey(userId)) { + supertokensOrExternalUserIdsToQuery.add(supertokensUserIdToExternalUserIdMap.get(userId)); + externalUserIdToSupertokensUserIdMap.put(supertokensUserIdToExternalUserIdMap.get(userId), userId); + } else { + supertokensOrExternalUserIdsToQuery.add(userId); + externalUserIdToSupertokensUserIdMap.put(userId, userId); + } + } + + Map supertokensOrExternalUserIdToEmailMap = new HashMap<>(); for (UserIdAndEmail ue : userIdAndEmail) { - if (userIdToEmailMap.containsKey(ue.userId)) { + String supertokensOrExternalUserId = ue.userId; + if (supertokensUserIdToExternalUserIdMap.containsKey(supertokensOrExternalUserId)) { + supertokensOrExternalUserId = supertokensUserIdToExternalUserIdMap.get(supertokensOrExternalUserId); + } + if (supertokensOrExternalUserIdToEmailMap.containsKey(supertokensOrExternalUserId)) { throw new RuntimeException("Found a bug!"); } - userIdToEmailMap.put(ue.userId, ue.email); + supertokensOrExternalUserIdToEmailMap.put(supertokensOrExternalUserId, ue.email); } + String QUERY = "SELECT * FROM " + getConfig(start).getEmailVerificationTable() - + " WHERE app_id = ? AND user_id IN (" + Utils.generateCommaSeperatedQuestionMarks(userIds.size()) + + + " WHERE app_id = ? AND user_id IN (" + Utils.generateCommaSeperatedQuestionMarks(supertokensOrExternalUserIdsToQuery.size()) + ") AND email IN (" + Utils.generateCommaSeperatedQuestionMarks(emails.size()) + ")"; return execute(sqlCon, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); int index = 2; - for (String userId : userIds) { + for (String userId : supertokensOrExternalUserIdsToQuery) { pst.setString(index++, userId); } for (String email : emails) { @@ -287,10 +311,10 @@ public static List isEmailVerified_transaction(Start start, Connection s }, result -> { List res = new ArrayList<>(); while (result.next()) { - String userId = result.getString("user_id"); + String supertokensOrExternalUserId = result.getString("user_id"); String email = result.getString("email"); - if (Objects.equals(userIdToEmailMap.get(userId), email)) { - res.add(userId); + if (Objects.equals(supertokensOrExternalUserIdToEmailMap.get(supertokensOrExternalUserId), email)) { + res.add(externalUserIdToSupertokensUserIdMap.get(supertokensOrExternalUserId)); } } return res; @@ -300,30 +324,51 @@ public static List isEmailVerified_transaction(Start start, Connection s public static List isEmailVerified(Start start, AppIdentifier appIdentifier, List userIdAndEmail) throws SQLException, StorageQueryException { + if (userIdAndEmail.isEmpty()) { return new ArrayList<>(); } List emails = new ArrayList<>(); - List userIds = new ArrayList<>(); - Map userIdToEmailMap = new HashMap<>(); + List supertokensUserIds = new ArrayList<>(); + for (UserIdAndEmail ue : userIdAndEmail) { emails.add(ue.email); - userIds.add(ue.userId); + supertokensUserIds.add(ue.userId); + } + // 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); + HashMap externalUserIdToSupertokensUserIdMap = new HashMap<>(); + List supertokensOrExternalUserIdsToQuery = new ArrayList<>(); + for (String userId : supertokensUserIds) { + if (supertokensUserIdToExternalUserIdMap.containsKey(userId)) { + supertokensOrExternalUserIdsToQuery.add(supertokensUserIdToExternalUserIdMap.get(userId)); + externalUserIdToSupertokensUserIdMap.put(supertokensUserIdToExternalUserIdMap.get(userId), userId); + } else { + supertokensOrExternalUserIdsToQuery.add(userId); + externalUserIdToSupertokensUserIdMap.put(userId, userId); + } } + + Map supertokensOrExternalUserIdToEmailMap = new HashMap<>(); for (UserIdAndEmail ue : userIdAndEmail) { - if (userIdToEmailMap.containsKey(ue.userId)) { + String supertokensOrExternalUserId = ue.userId; + if (supertokensUserIdToExternalUserIdMap.containsKey(supertokensOrExternalUserId)) { + supertokensOrExternalUserId = supertokensUserIdToExternalUserIdMap.get(supertokensOrExternalUserId); + } + if (supertokensOrExternalUserIdToEmailMap.containsKey(supertokensOrExternalUserId)) { throw new RuntimeException("Found a bug!"); } - userIdToEmailMap.put(ue.userId, ue.email); + supertokensOrExternalUserIdToEmailMap.put(supertokensOrExternalUserId, ue.email); } String QUERY = "SELECT * FROM " + getConfig(start).getEmailVerificationTable() - + " WHERE app_id = ? AND user_id IN (" + Utils.generateCommaSeperatedQuestionMarks(userIds.size()) + + + " WHERE app_id = ? AND user_id IN (" + Utils.generateCommaSeperatedQuestionMarks(supertokensOrExternalUserIdsToQuery.size()) + ") AND email IN (" + Utils.generateCommaSeperatedQuestionMarks(emails.size()) + ")"; - return execute(start, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); int index = 2; - for (String userId : userIds) { + for (String userId : supertokensOrExternalUserIdsToQuery) { pst.setString(index++, userId); } for (String email : emails) { @@ -332,10 +377,10 @@ public static List isEmailVerified(Start start, AppIdentifier appIdentif }, result -> { List res = new ArrayList<>(); while (result.next()) { - String userId = result.getString("user_id"); + String supertokensOrExternalUserId = result.getString("user_id"); String email = result.getString("email"); - if (Objects.equals(userIdToEmailMap.get(userId), email)) { - res.add(userId); + if (Objects.equals(supertokensOrExternalUserIdToEmailMap.get(supertokensOrExternalUserId), email)) { + res.add(externalUserIdToSupertokensUserIdMap.get(supertokensOrExternalUserId)); } } return res; 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 a32dccb7..24f4fab7 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/UserIdMappingQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/UserIdMappingQueries.java @@ -30,6 +30,7 @@ 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; @@ -127,7 +128,7 @@ public static UserIdMapping[] getUserIdMappingWithEitherSuperTokensUserIdOrExter } - public static HashMap getUserIdMappingWithUserIds(Start start, ArrayList userIds) + public static HashMap getUserIdMappingWithUserIds(Start start, List userIds) throws SQLException, StorageQueryException { if (userIds.size() == 0) { @@ -160,6 +161,39 @@ public static HashMap getUserIdMappingWithUserIds(Start start, A }); } + public static HashMap getUserIdMappingWithUserIds_Transaction(Start start, Connection sqlCon, List 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 supertokens_user_id IN ("); + for (int i = 0; i < userIds.size(); i++) { + QUERY.append("?"); + if (i != userIds.size() - 1) { + // not the last element + QUERY.append(","); + } + } + QUERY.append(")"); + return execute(sqlCon, QUERY.toString(), pst -> { + 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)); + } + }, result -> { + HashMap userIdMappings = new HashMap<>(); + while (result.next()) { + UserIdMapping temp = UserIdMappingRowMapper.getInstance().mapOrThrow(result); + userIdMappings.put(temp.superTokensUserId, temp.externalUserId); + } + return userIdMappings; + }); + } + public static boolean deleteUserIdMappingWithSuperTokensUserId(Start start, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { String QUERY = "DELETE FROM " + Config.getConfig(start).getUserIdMappingTable() From e5c2e0023d602dce441c2411ecbb347ec09c8d82 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Wed, 1 Nov 2023 12:41:29 +0530 Subject: [PATCH 103/148] adding dev-v5.0.2 tag to this commit to ensure building --- ...-5.0.1.jar => postgresql-plugin-5.0.2.jar} | Bin 206684 -> 207469 bytes 1 file changed, 0 insertions(+), 0 deletions(-) rename jar/{postgresql-plugin-5.0.1.jar => postgresql-plugin-5.0.2.jar} (75%) diff --git a/jar/postgresql-plugin-5.0.1.jar b/jar/postgresql-plugin-5.0.2.jar similarity index 75% rename from jar/postgresql-plugin-5.0.1.jar rename to jar/postgresql-plugin-5.0.2.jar index c06e9367c873a94d471fb9876b6c5a66c68cf24b..54a5987c2de72293b7c05eba25cbcebc6b3c19ec 100644 GIT binary patch delta 44258 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 delta 43543 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 qA?n(33p9og1-{$@eS(F~;s8If$U Date: Wed, 1 Nov 2023 17:46:33 +0530 Subject: [PATCH 104/148] 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 105/148] 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 b42a4708a42d35332758de402c361ec088abde02 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Fri, 10 Nov 2023 12:30:57 +0530 Subject: [PATCH 106/148] fix: email verification with user id mapping (#172) --- CHANGELOG.md | 4 ++ build.gradle | 2 +- .../supertokens/storage/postgresql/Start.java | 5 ++ .../queries/EmailVerificationQueries.java | 64 +++++++++++++++++-- 4 files changed, 68 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eeba6da2..58479d46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [5.0.3] - 2023-11-10 + +- Fixes issue with email verification with user id mapping + ## [5.0.2] - 2023-11-01 - Fixes `verified` in `loginMethods` for users with userId mapping diff --git a/build.gradle b/build.gradle index e682b205..61405636 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'java-library' } -version = "5.0.2" +version = "5.0.3" repositories { mavenCentral() diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 505bb9fb..b3ad7075 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -1182,6 +1182,11 @@ public boolean isEmailVerified(AppIdentifier appIdentifier, String userId, Strin } } + @Override + public void updateIsEmailVerifiedToExternalUserId(AppIdentifier appIdentifier, String supertokensUserId, String externalUserId) throws StorageQueryException { + EmailVerificationQueries.updateIsEmailVerifiedToExternalUserId(this, appIdentifier, supertokensUserId, externalUserId); + } + @Override public void deleteExpiredPasswordResetTokens() throws StorageQueryException { try { 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 3547f084..ff9fc950 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/EmailVerificationQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/EmailVerificationQueries.java @@ -19,8 +19,10 @@ import io.supertokens.pluginInterface.RowMapper; 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.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; @@ -450,13 +452,63 @@ public static void revokeAllTokens(Start start, TenantIdentifier tenantIdentifie public static boolean isUserIdBeingUsedForEmailVerification(Start start, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { - String QUERY = "SELECT * FROM " + getConfig(start).getEmailVerificationTokensTable() - + " WHERE app_id = ? AND user_id = ?"; + { + String QUERY = "SELECT * FROM " + getConfig(start).getEmailVerificationTokensTable() + + " WHERE app_id = ? AND user_id = ?"; - return execute(start, QUERY, pst -> { - pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, userId); - }, ResultSet::next); + boolean isUsed = execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }, ResultSet::next); + if (isUsed) { + return true; + } + } + + { + String QUERY = "SELECT * FROM " + getConfig(start).getEmailVerificationTable() + + " WHERE app_id = ? AND user_id = ?"; + + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }, ResultSet::next); + } + } + + public static void updateIsEmailVerifiedToExternalUserId(Start start, AppIdentifier appIdentifier, String supertokensUserId, String externalUserId) + throws StorageQueryException { + try { + start.startTransaction((TransactionConnection con) -> { + Connection sqlCon = (Connection) con.getConnection(); + try { + { + String QUERY = "UPDATE " + getConfig(start).getEmailVerificationTable() + + " SET user_id = ? WHERE app_id = ? AND user_id = ?"; + update(sqlCon, QUERY, pst -> { + pst.setString(1, externalUserId); + pst.setString(2, appIdentifier.getAppId()); + pst.setString(3, supertokensUserId); + }); + } + { + String QUERY = "UPDATE " + getConfig(start).getEmailVerificationTokensTable() + + " SET user_id = ? WHERE app_id = ? AND user_id = ?"; + update(sqlCon, QUERY, pst -> { + pst.setString(1, externalUserId); + pst.setString(2, appIdentifier.getAppId()); + pst.setString(3, supertokensUserId); + }); + } + } catch (SQLException e) { + throw new StorageTransactionLogicException(e); + } + + return null; + }); + } catch (StorageTransactionLogicException e) { + throw new StorageQueryException(e.actualException); + } } private static class EmailVerificationTokenInfoRowMapper From 5039fddf822c756c3521d23e527b9a9bc042241a Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Fri, 10 Nov 2023 12:53:07 +0530 Subject: [PATCH 107/148] adding dev-v5.0.3 tag to this commit to ensure building --- ...-5.0.2.jar => postgresql-plugin-5.0.3.jar} | Bin 207469 -> 208288 bytes 1 file changed, 0 insertions(+), 0 deletions(-) rename jar/{postgresql-plugin-5.0.2.jar => postgresql-plugin-5.0.3.jar} (77%) diff --git a/jar/postgresql-plugin-5.0.2.jar b/jar/postgresql-plugin-5.0.3.jar similarity index 77% rename from jar/postgresql-plugin-5.0.2.jar rename to jar/postgresql-plugin-5.0.3.jar index 54a5987c2de72293b7c05eba25cbcebc6b3c19ec..4a4e598b5947bf36b3df3f738e6f1c234d51ac43 100644 GIT binary patch delta 39260 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 z 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 Aga7~l delta 38363 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 From f4c56ca74109b0210be9284c0787205b4884c3d2 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Tue, 21 Nov 2023 16:11:51 +0530 Subject: [PATCH 108/148] fix: add primary_user_id index (#173) --- CHANGELOG.md | 12 ++++++++++++ build.gradle | 2 +- .../storage/postgresql/queries/GeneralQueries.java | 6 ++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58479d46..ec788311 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [5.0.4] - 2023-11-23 + +- Adds `app_id_to_user_id_primary_user_id_index` index on `app_id_to_user_id` table + +### Migration + +Run the following sql script: + +```sql +CREATE INDEX IF NOT EXISTS app_id_to_user_id_primary_user_id_index ON app_id_to_user_id (primary_or_recipe_user_id, app_id); +``` + ## [5.0.3] - 2023-11-10 - Fixes issue with email verification with user id mapping diff --git a/build.gradle b/build.gradle index 61405636..bc639aae 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'java-library' } -version = "5.0.3" +version = "5.0.4" repositories { mavenCentral() 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 976c3337..0099c2ff 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java @@ -231,6 +231,11 @@ static String getQueryToCreateAppIdIndexForAppIdToUserIdTable(Start start) { + Config.getConfig(start).getAppIdToUserIdTable() + "(app_id);"; } + static String getQueryToCreatePrimaryUserIdIndexForAppIdToUserIdTable(Start start) { + return "CREATE INDEX IF NOT EXISTS app_id_to_user_id_primary_user_id_index ON " + + Config.getConfig(start).getAppIdToUserIdTable() + "(primary_or_recipe_user_id, app_id);"; + } + public static void createTablesIfNotExists(Start start) throws SQLException, StorageQueryException { int numberOfRetries = 0; boolean retry = true; @@ -264,6 +269,7 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto // index update(start, getQueryToCreateAppIdIndexForAppIdToUserIdTable(start), NO_OP_SETTER); + update(start, getQueryToCreatePrimaryUserIdIndexForAppIdToUserIdTable(start), NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getUsersTable())) { From 02c716b6e901b5560ce4379b1e6d058824592eb3 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 21 Nov 2023 16:12:39 +0530 Subject: [PATCH 109/148] adding dev-v5.0.4 tag to this commit to ensure building --- ...-5.0.3.jar => postgresql-plugin-5.0.4.jar} | Bin 208288 -> 208342 bytes 1 file changed, 0 insertions(+), 0 deletions(-) rename jar/{postgresql-plugin-5.0.3.jar => postgresql-plugin-5.0.4.jar} (84%) diff --git a/jar/postgresql-plugin-5.0.3.jar b/jar/postgresql-plugin-5.0.4.jar similarity index 84% rename from jar/postgresql-plugin-5.0.3.jar rename to jar/postgresql-plugin-5.0.4.jar index 4a4e598b5947bf36b3df3f738e6f1c234d51ac43..de32a20ce5785fc1395a2c778059f35e2edb2dc8 100644 GIT binary patch delta 25713 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 delta 25561 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ævq>QkT%FC73Pn?5Q>0+;lrD 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 From 96777e6bbb4b0e6ad89bae9f564d1aa5c35d246d Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Tue, 28 Nov 2023 10:09:17 +0530 Subject: [PATCH 110/148] fix: fixes call to getPrimaryUserInfoForUserIds_Transaction (#174) * fix: transaction * fix: version --- CHANGELOG.md | 4 ++++ build.gradle | 2 +- .../storage/postgresql/queries/GeneralQueries.java | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec788311..382fc935 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [5.0.5] - 2023-11-23 + +- Fixes call to `getPrimaryUserInfoForUserIds_Transaction` in `listPrimaryUsersByThirdPartyInfo_Transaction` + ## [5.0.4] - 2023-11-23 - Adds `app_id_to_user_id_primary_user_id_index` index on `app_id_to_user_id` table diff --git a/build.gradle b/build.gradle index bc639aae..894551fe 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'java-library' } -version = "5.0.4" +version = "5.0.5" repositories { mavenCentral() 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 0099c2ff..729b8a51 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java @@ -1248,7 +1248,7 @@ public static AuthRecipeUserInfo[] listPrimaryUsersByEmail_Transaction(Start sta Set userIdsSet = new HashSet<>(userIds); userIds = new ArrayList<>(userIdsSet); - List result = getPrimaryUserInfoForUserIds(start, appIdentifier, + List result = getPrimaryUserInfoForUserIds_Transaction(start, sqlCon, appIdentifier, userIds); // this is going to order them based on oldest that joined to newest that joined. From 28ca3e7e2206c8b70ee2eac348c1e10776430c7e Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 28 Nov 2023 10:10:07 +0530 Subject: [PATCH 111/148] adding dev-v5.0.5 tag to this commit to ensure building --- ...-5.0.4.jar => postgresql-plugin-5.0.5.jar} | Bin 208342 -> 208337 bytes 1 file changed, 0 insertions(+), 0 deletions(-) rename jar/{postgresql-plugin-5.0.4.jar => postgresql-plugin-5.0.5.jar} (85%) diff --git a/jar/postgresql-plugin-5.0.4.jar b/jar/postgresql-plugin-5.0.5.jar similarity index 85% rename from jar/postgresql-plugin-5.0.4.jar rename to jar/postgresql-plugin-5.0.5.jar index de32a20ce5785fc1395a2c778059f35e2edb2dc8..55a0fb605d46abe85d7f09693cbf2355b6299fc2 100644 GIT binary patch delta 24278 zcmY(oV~{1j(*`=WZCg9GZDWT!w(T>v&)BwY+ctO3aK}5=-rxVdA8y^QB%M_EQ1H;IF$M8I9ukTE%g_P*zkj{mpz}qmZ#KNU=05j&YXPy-UPhl z`@QpZ^k&coI`p7|l@kE&!J$vV9YerEMVF7X{!~k^X{|L7Xm$U-6ZJp@H`8+nZ~)98 zK`;rKxv)P!WTU|fL-cKE*TI8$R&{jRn5rCf!2va`_* zv0-VbybE*Sqmv|!r5#2pPuIsqH55j&MFaj;o~B2XS8D~U#Usx#*JMM?$*jifZDLv1 z+9Ir`c4)Rit;1aEnHu?dIF;ffjTUzx0~GHhenDUgM7kT>71~G) zB?n(>`)i~B-1!+<$q;6ay)TqZ0x2wOtTD4{jeo_Q_#A?*9N(DgP!0QzDGN|rL%4v9 zMM$Vc;cQ>!UiC933(i6>;aYF2ude%1E2F5btpzI$9a5K*EF2znnqU7c#(?xZh=v&< zP7th?NBL_{b)eaEt2^K?15%>aN9S}~keN6B;$Pat9j_hYg};pD1uG{Tb#;;DzXzZ=4v zd>Z_TIX>hK!Iww5wymzeA!wW2i*fGQ)P%29)ZSlKYRg=GPtKxjwqR>u{V*(vWu#C@ zLab@w5RXekNzRB(K4MZ0ze3d%og!2%mt^qh`#oO1-KM2P$UvCQkOP2NYl{z_);5QO zNH(;}N}}y4H{R$SA0N(+fth0YYy$(86kVQ9o!cOm{>fE1boz)}KS;8g8XU|)GB673 z#e4XjjN8yVIu(oBn}Uj{#H;!+)cR_C{{#Pbk%78_n6bJdR*$?R+jwF1xb&DfCQ1s} z1g%5}*WC~e5e2OBa4-NB&R&&88g(BTMSgrTM3!dAbqf*uTV;Up*f49`&@`2)33X5& zwU*t4Yn39DpQng&F=GuTTV@V%6*jy25~Ve!vV#j5JoXq`W}SNmOwi0pTzs6Jv^JO@ zwU(=hga9rsj#`>i6E-_XD~b#)ahOhXOOz`eWxpeEnGdTRO$Hzegw0m64%UWP>0TFd zFwwOM6!Y=1@R2f-0*CqdY!2y-8BXy@qlQ*sV0^H|H93i6`Hh9*UJW|&#Y_l8hU(Sg z;>wnaH8YWiH6}J~DcM<#8&j`JU_Y|vg%QG)7YiSViTErB3u#2wpkXsil1q)4q&nrr z{DuNZuh=`(ssXTwKV-_u@xxOdFco>#)b~YFTq|@kkX5*ku(nu7!llBw!k@T;DbJ?O z8GAQuXmVNnkfW}!Prc1&IFxu5+Gky4s5m7x7u*zib)JcS$LRiaPo7)gIfB^o*NhD7 zeCeAEi?uk2P7c7;Ltn7nXNyx%>Qo#)CxoX=<(#zC$pqZtZZXZ(Z4EQb9#xk@)HIDx zqUdBfd(;xUT146@ZerO*HZ_kND0Z@}qa2$@&e7~1GJLUmPNnl?Ik(u&u&0Sw=TDz| z)W8EWmTEGTTVkrS^&hJBZ8xB?e{*;4L3(96SA^k)dLc%@Ax2?@5Yt3-t6j4H7E#({ zh$P%AQU-K+lA!D;ZnDr5&|wDKUi2|gHJ`NATrSP4?S;y_@a!H3`zdv*psa-->Lie< zv2bhYs{Qm7yo96?tc@v{5jA(L}DHZpExb1P6_K`_t^=r z(wW`sXy9ya=CA@&$Kf@iK_ht0kS}|HA++x7y$%RF3wf(v+s`J1lJf&MdK^@s>1C41 zB)hfy)at&Ck`mt%`#TDlp8?ri9wG?~^5=9GC? zF6me*aF@`1!+GSeyCq7a_DzVd1%!i>?s;*%@Q-&(BOLCL08vRuzkyut_XTp0LAq>x< z4k0#4afjz-aP(S5jhy6^(`MDuYb>Q;U;q-aFH1;gnYV`RLmPz$ZB+}?VRWbx`ZXE= z<2=@SScwy_qQc^MrosxJVCs5NBmiw1AYkbvtrtCnfY{azw`h5`d3KE#q8E?J1%H-F zN>;OmrPcKH6?ZNu%e=jmX)U}NPAF6IG_$l&0`+nqGcwC6w{%Ra3VMJ(-vDE~;y3tf`l=!6l`UG*462^knMsi*4cXZz|=LzzFVA*%`D% zR%R)B>rh$N$z6sb9$FxK0JupBr_f23%~LGlz2rA4Rh{iIibt`96>H?yr`V+3qk2kE zDFRC6xMfQ}?BFXa5FwT9?A8>?pg!qgdett#HNs_1Z5dvs{mz$;A-_=>&#D?XxY$f;7(SXn$=3Z{ZU!SG6xt4D#k_5?L?Y=QY>RXip&ZL36Snji)m_i3A$S$#Rjvi}atd(ke-zF(L25fHEIO!Qi5;v?< zdG#}dN2ZrxkW{w?G+P9%-7yjA(zIvoQ>*3z!S{xf(h7_?+dk*qt{rkW7rQOt9G}9# zT_>w4f|5Jd{&+#_lt_b-PW|IZrH81RV3`^>bIrh2s>CgT9UTq}*Su;^yr4-6?ap5X zbfp6BDURzCD{T#hLF_iW(0v<1pLwc<9vUpQNtw9kVmYIjwK~3T-$_O&-_9%E*U1?W z7qFLQ>qsJVtWvHL%IJXxyhPW5{bN%3p*^zKEX8YTU<4)er@}v|-ZkVWX05g->)x;2 z9k;id8dWZUTJ&WNqy|U2&y*ij_f%c|>U%``YS8ZGL&}Rw;rKeXN86ffA*#{?`Ws$N zqkh`1wX$5Iuz6Aw6TWW{PlCVn_kx5l&lE05VRamj=Lg*A8n+Hqwm#O4c~+!|11sre$bop=CE?duHjy zQH)`L)&WK(q4EHPcL7<6>)d$dZ2bew{oAlJ@S4{E&HMal-YFG@-!^FxRuA(G1gxH# zftCP;Z3);8p5@ z>aRp$1;?9jnw;|0LK4)cY|eyt*5h*`W4NMlWMq%^PdZH*W9U@Z@`xvf{FVIH4!I9i5qU8ZroqNgce6v9#fgiQYZ8r><7TF zQZayXDQ{0tUkB4#K0@s9X$dLYEToW<(lx(O702(wTJc;XWA+CfqikD(Mwzlo9=*V6 zwmTECRbDf~YRNlj)ICrqQDjYlwlJdnG;LOHjSn(}g-K57WmztheC~0X{uOv#wsJ=+ zfxN-e=K2PELj83Z63uE=uPG_Ktr&cCN)gK$}xDw%RyemuH2uC74L z%1hMJ+gAQMDRDa&1kP84^y{!jJ8L4L{ z&4`k7E8*woWcbo?sw-ubraB?oT72pN6Am>XbfdGX85v3 z@!(|{5L^`4ZEXejM0tGA)o6sWNibV!KG+h1(8}}~-91va)K^>w8H+x*N+(t@;qFei zWCIjj93GCkHZHcl)fxFQn76CN!@q}PRsn1!GKurDbFrDB zx;NX|B9zP(Igd#6!(!=3;`-WZ5O`7x!x!j6IfEfWxg=fXgC*2WW*}-@tH!7$XqrA% zSDtz{Z)tM;@#(W;kVS8=V&m}02~D5)!+0g)YJ;urb(s#vT`3C#JrlE-vVoHEPbykH z`v;0m=Xg0DIc}u(O~B!9c9iL;WK}efiUi0U$CL926Wz3hx6L3{S}{qZabl63x>3hl zl`vYd#Fl_b*$gzwM_M@%Ofk;AXp?Y_y z^!GV=Gx9|p@5MLrCH+=Zh!FIl^5G6p{r!p-?6uN!m=!x53YaCKDq^#8ZJuG^6WjE& zJee_Om5FH+owlm)oh?a}gArs|d97zP7NK(LkE2lWxcPY!6XWsjq?sR3qy2}~BW?dL zK2TVpfHG-w84c&t;DF~+{3>;&3Rm%+B4BD8$CX5TwCEb!?{S9K%S5@YV@MG6$w9#= z_?Tv_sh#GU2x#i8UXu9*Llf}%hbADjKY9ob*1B383I0*uOPqf72~Wp!xkKoY7}(F) zr{(I7`507FldS+D(>~e$kL|+maR5)~vWep11drIs|55HeSWX1V(OudhY;+~@f|lgH zN=~HYUnED%VxcUQZ}|9ndQ=BFr+G&B^f?3k7a~6G03gnLrv8g%N|kQTDOgbtB`JeK zm#O?fj_@;okjwGjWNFxI4dp+TqoKTUwz8 z_aUe!EHDG9Eae|!mBF}aq55&a)W3kkRg_e?^rbyv_GlL%yd@gm&kfZ+X=2C8nuexa zJKlzdT|k33@r5Q8y%BIsPJQ{ZQQEI>i;xJGI~S^GzR4Sy#Wb{?W3%BD;dT@wl8s2r zyKYWr6-}dHy2mCrQDwCk(QBC{XMBo#Dx%ZA&S);J#W^|Q=HX&1_-Dx1_h-zMZ`1zy zm$=Xa;d-&KE(%wX9=0k1&Q;X~TaYITDFh8I3}Du&xxcnR@q!R^8Y{BbS7o5FGYFGu zPevGKL|js-_@?TaX(H(-m%)!psAz0iaUmNSOte-@N@&~6$sH#;z{=%Dv@{k0#qy6h z)IsqtQboRIqokZ%_Y`}kAc)C9Bsa)2cTnWp`stRUDrMRpc=A)T{l^d5w5dk8Ey#kV zB>)ZKh^vb!!&XW8@zHFjFTgZn$FE=ZlUFX3yziJ-Zdnqjl)~aF88%Tf1cK?2M5lLf zjF~ELwsC!GET?IxXPK7#bIB=$KLg^$JZa=m;#NrD5OgJaF%h)))Rd1}nx1&rB*KJZ zJU3n(6ZXZ+0V2gR7hz}Hg9{Au2dvFc0-)yK zlO~j`m@y@jm|-yT=!SzF2b^yrq_)g?CtZIiD0-sl+Y%QynWJ6P^q5L~_$tuS5Ds%5 z83T!jQQ|51nUqv>&;U^*x9$&{E|LlnW$c6o(q6P9L;w9mf#FltC;rnKI|hyIfF`Lx z#2{$J_LMsHiUv^6)I;j|f7OW-a1RIyG8HQ>1vjR8TfG&cwp6PIN8({yr6{vV%*ubr z3s{{f0dzIMw^u!jPxQ;L3CrV1F?N1_V-ioOU~0ejbZ>(0Q|J3j>ee@8S4`Rju4YSM zTch@ybc6PmIqt8~^%WRY0d{*+0HYyW_j){yKn$@%-jX_`EVQ~~TjRLiq)=52IvZTz zt>hubBaggO+tSE4RzOl6%^QK#a8#LN>*5NDv)V)bwT$tkc$~cgP<}ZF-vu@_ z&%U*FMU<-9#@{P-_=7k{q0tG~AUJfj&x0J}_^+a;a8HmZk0J(9A-v*+yIG}BDeKX{ zOhfbL*e&YGXs%%76W5GFzfDc)YLsfGo>}vZ7DiB3D-LK=&u4LVYqmNqZls_}*ps?v zdZeCv>(>c;Eah!uIl`PR;Nm4}eY;##P9i|g&d`uuvssCzC;clvNR@*Tf-=vsw6of! z`VZC&TUU3!!*e5{q9iAe>ZgL&=@?vx{tS(X#N+QqVFSsZRE;A>gjCRTkK0s3)1v2O0$A z4`v5^b)}%-x5K0eT~V)s;W|!%9~7H->Q4$WiW!R9@k0$Ca)#HX&(9tGB+XV>YvjlJy@Rr z$n3TWRq?rKm;Gt|GH_~<>fXunmPR7>>K#8A@>!6&rry!I&M8?g_|evtHY#9RID=#R zhxun>`Y?M})GmYCPgoO!Y~YYw z^EG|RrZq9#020a3O`6d;a8ZUF;NWJFH=O~h=x9{c!vDm|a0fP5Y=B&Lt%!v-_>=`E z3etVL>r+T(t7@45F3ox$fP^ba+rVyGZW0Usye zOR}E?(NOKvL;vgUKF#yPH`MR{%0n;=s5*YQ7&*Z-q@x;ajuhPMF;$EJenkJ;WK^-W zLkW1S+oLOMUH7$+xG|SUIpJB*$QvBUJhrbXjpA zLRJ4zSU64P@7)E>>R)^Sl>GB1MjkBu48i)$Mh0lzPFJzO_XKfxY(!E+lin7nOrGTh>pV$yHHl!v0!F{V=LDcvwS%h(70gEROlE)o@jl@X*@( zU0trHyPno-;lWJfrtzGaS{1bt9E&iWUMx@6ixr6{evm)RJ+}_{KySL6jgb~RZW0E| z3`$$GOHv_l`c?CVfW}>m5YRhq@8X{c|54`>C@DA3MnG7HihU8YkMrSvWuUvfi1Dec z=ZQ;kW?HKH0PFTK>O1zhoMsV>^QVqOJxV2~F-#?ZTVu6j3&JBfJ&3sbzRCZ(Sp6?h z>%aVQjEj2gs(~7C38%ffG1=Wd8%{~lp-OAJ_MoSnG0$A`*Oh3J#`&1ElXs)-@A`UG zE86Ljl_FqbY^A=b_oe!Ie(<2DATRiq!6JZp#hLb|_ba}0EFrPzbf$Ig`s&lEsdOi8 zqz7>kb8UJ9d^i#9dhYH#-1bk+A3eLPhK#91M-OoN+@4lwKlj{0DWHoyE_UpoB|!MJ zS@ASLX7xm2HsKrc>xvziCr=A-%OO}Nu`ZM8PiuS9eu#qpSNsst{2zJH+x+>KhVAp0 z;jeutT!*2vhXlc721+$*;maxs$)f)32DJk#|iD7G$u8d3+%T2w^} ztf~3+u_mYN=i1T8XcVX4R~Yb<>t&LWq}23NDlJDoU9O(b8F`{ zN!%pKdEsiWBy8O7%_l)^e-DD38~C;;(cZw-Jxq()ejj@@yNX7V%)HAhZkkld<^;I@ z$rEwWn2~uC@4J?@E9op%sbMTU*`^ zbBpKsj>26q7|{Z4#RsrTz5Pynh8YkTW6y`)Kxm6a@=5~aJa@Di@@$BV-V^w>9D5X` ze}T$qH&(ckS3WXctUh;%HM20vh*F*Tn<=C-a$_^W>!uCWhP z>*9n02&GBhE{ofxNbPtM-*UQ~zEg3LHN38Zpg!3I?73l{yOSWOR9XQPF%kC0f(Wx{+GL zpxd7ajjO5A>nSd0V1CX>Ss72HRPAsYh`_{9US(r$+Z`u3f$;m6kc~dMT0035iq$C6`bgPIO>*-lzcd4KIX%M~{UhQ2raDRe(^Lsc39<30sk9*<9JK z8;}mLcaLD#`?DS3f7r*tV(Ar?;>M7~?WnksU4kl$%5`hpoyzUm>>u05@uyXbS-a92 zi2U(gQkBQb$rEOvNUAAID8TlZ2YhZ*cf!u$HFYbb>1SEHZl+ef| zL~-1xA z#q8y8Kc{*WvB}%IVO{%<+4Nx)+5H>9KY_s&kmjB{vHN zzov+~DY%FR;HQZ$UqwWuTjmOJULsFPN-*6;LrNygVCA6E%CpKx!u%IaN45fNVg6rw zdREJ6(UHu#iUaIV1kU1{*3-kHxSpECNcQ@8f(k^KN2(Q!p-QK8ay~jT#Xxm^J<$o@ z8Y50|>~h0nLX*(}_0a7AMW2PR8ViG)- zG0II>l?s72az0YJ`L&c)h%*SWf2Y&SNmLs-CpWGZUv^qTe>_-)y=3d_X{+)!NIu8h zq$cZWvvw5L7E%oilGioDlu=KH6hovs2O9YV?&pTg4|OmmJV&&U-VPc2A@M^Ji&6)x z*9|-$P`REr>>N-7#EsVrC{xhnR(avD@@#x?gSLA;Aqg>{AnIwHUGNS!za5t|Jw@9Y z=_;h>>FE}3MT;-HMv;%FrTl{QZ}tzS?muEMQ3dc~2Qx@NXP;Y_f`DYfNX^ZvvDSV|*9Dc`s*#>BvS{#?G5 z)6s6D3xo;q?&;T;SddNYBod2qBK`fFMDB&}#ss{W1NZtSYsY0W(^vU+(Cadzk&zj1 zNj!s04<7%k=Y;CgjAdSvOutXIKu^tCbxios(X*Q9up{9-#qJM0me=6xf4Nm!OfySZ z4RTm-BdSLa;48JhrS$4W0aajOVaI1br=?$AV{V zq8vTqjB@-PA%5k$)PEu3ij`$W#J9$CZ(z5hX@cGJTs^99SqB)5{j+nKbB~5TR+@ zi9$5@0D1U31`S2*d!u_Dh!#$%x3XAyhUK$D44ic1Cdgl8|Ev=}o#V&BO&n8L8@R^I z*`O6Rrk0<|GpWXDqwjyZl+<=kVPTF+GVM_)bYz?6;@x8ZBV*-v6&p<}FNAE1>NFhg z9KkYslZke@+MxQz)^d=bk4K5JC)Hc}WShMRP_1G5-5 zb*vY3({mT~yvWxzg5Am4!$qHyB^3?aRGjpm3&Y>_z$di(#0aZnl0EWQ#ekF_)g?gr zjQ*mDn>`66=ZjiW-!Vwu4n$YJz5t|9{~NT#i)FVdZO0%@EnBrCaA-J$_(mWNzR^7z z09-!8XKV!OwNdxM5muTOL*fiC+k4C`Y`6PG56xGmv3N*NBtZ&$`vSX1xD}Snv&M#qWEAt%5#DnaA~q zXqE#CPfiNbJ0VC6%hKo`A~+NvvE26?Kn;OE8V+4;utmpco=BF`fHvt~)|U6&fG+;B zzfC~-kKKUQ6K74>6EXINpvOV#rA_M<8RBfq)gD(!pw=TOOv0fgN6OYCg6fTTov z%%9i7i1nRjK4^=omW*+TSoe1Cj2QfRSoV>(MXZuhYo-7ug@R1eQdwCHc%yp&RG903 zS09=j_DWxGM4c;Dory}f8%tqjUZdAWU5!G-{wjs;Mn7LaCz`$Mv%vgWr+gBJ@8`R8 zdOw#`I_Tr&yd5B6={J_hE^R#p_?9dT|72r&mBRR{p35$nuG{Nr`u@J zEIzHuV;V93bQ<=tKhwaJm^4AsKpLrvHu1KgpU1M{rDgjK&*>4cm;jefHyWf7GFqHN z9}z?TQ$D5arW}tc!aJS>s{RjwGGonUwtxf!O_|sOQ5oK(Y;x2KyHxc#;DqZP&3gYO zY9d7XSG2T<1`w9Z z_|2&{F@e>o%nE$v=QZ*r<~FVLzOlkalo^Bm4t>`^49g_t_u((}VG>%CL>{S_^YF6h zX3i-rAt{5DsJ{dn@O)aR0PQvYWlwnTgQabqKa&aJMcKSON!8hUGW<9xwXUszD%NVjcHLMDNE(l5azC@F6@1EN$GznT6yr(?Xz`&RLi;~H5#f0^0fKy+{u)%(MYxm8 z;N`4k9LzLH4ft4k_=zrTwD#9CbZN%8*~7FO{c-HN3_RgFw!bMLhCJ}}m&8q|I@>9!G! z7{h@XQ?T+3U4 zK|un1ShfYj7V>g@phP)^-hl0pS-=mJW$qQjTtoL20Sdl}!4%^c5tG3(6TU1(5oSke zTMAJ@Rp=b(^{^@4Y<6Sc0caH%M%83bCx~ysF>(F9NCg;1jwU^$5=JmT3}7a#cfX@E z<#**dI))1CSZgQW=NRVar)j1rJBH3{uR!oqz`Jo@cIcTE;*+eO`PWPxLqtJ4_Bioj z-o5i<0I@SM3@i#U3syJ=V>%}3dY0g(Myx44m^Wv*RM8>|rZsUn%wVCZoD{e#5ugs8 z>G33IPFR*6H$1@;H_=qEC=YZkLC$JnTs3jC{37INauzXI4k@xd7k8BuS&drYM+Y5Q4b|tSCwp0H%2#d(wj#j6Md&wUsf6kl)zZg)>xT zn1+@z31|?!w+r5wVr_h840J=E0 znVn(!hJXi`!LR9^6z}GwACCD(Q8p~hyF~I%Wu9*@!r*^SZ=&5fX<||4V9k%B2_ruN z2zhuOZsQ&Gh?lisIrOfHEYU_&N~v*q`v&AN_bE z+3?2gJLknp)_Y#)SFZFTnRBD|Nv8AyeZ3Ta<159vM`{d2P2NW}xW4_35~L*q`GJ+= zA0aC2;nwF*|0|*nJ@Xd9-T}-T#dPKM@jJt3dh=0nF3&;Ljqh=6_{d9?atq-P02CUJ zC)F|3q~KmcU%hk}B|RZPkcYKzl4TdEK^#91cgw2bF&TgE++MQelkkB!B4AZD&q%@l z>$kOg*YUUf#Mdw4mks`#2Cv-%JmF2|`qHnFC2Ixm$Q}cMRz89~()}ZH9V5Zt){mLT z-&1$<3!T${-|nOvI*T8>OP2JH0L1$)*LS2cB`+fa{-Bugilca8FNep0JL!W?*JD4K z62pX#U&H*E&LcUEs+7NxZ_#eK2`*007x!EaIC|ZjHs%9}iD7teG)4qaJ(LSJ{q;$GeI-mzGRojstzY&vfhQP9k}-YY66|tToSr^gx|IJa#j}0g!qi`5mzY z6j*^9RK#0@lkKkYr6TTLi9EYV7@5fQJCr|$d>78CAiRmMf?uihu#%TW86LM#6^`fcWLd`Ny0{N=j}WXpICEUk9q2F;38WO=M8Kn&6@TZBfk@ z{^5iAa8L&doiQma=tx!pE00nfSWHr;3R5}w93kd=IY>y}I5Q!EtO&AomE$onLb;>1 zg!?yA1=7LW9vE*u)z6g=jLaH;$+Wo|t~yiUg?`7kvhOkBD^gVf;MtQADN~T}CcYlp zVxaP%W45PAdPh+Wag@ULO8E{)QbG6A1Q_$&1HG3s9%;TNRhJ9mC?F>_Q{)k*jMc>= zl1|L=swnZzDW|AtlbKve@a5{M%qHikGVH0%rXevB8b0&8ZwL2wWRK6lgIc|!? z`5H*om!qn9XA0q+wW@p}87v^Swv;qk@12-07ri-xI?4BxR;CfXH2NgPk~_4u8QPp! zUy-|!pgEZ>aN%gU4ZP={gA2d*q1c)V_ z1RzQ8b6`xtN{c*1CyNPljVCX}>4Mx!qWo=$B>UO4Q=H-{C%wkmKr+Xi4=pb6Z)2O1 zfn49p(M!@@_juUjv8$~6MMlRO1XlhkYX7W75&>eEf+X8>9J!t^am7>l^sSM!1<#LI zD_9HS+cO%{ettMh{rS}HLDTqFK}S=O=7Fr1EyElL5j98|eu9mv%s#AHT$gbvzBqp+ zUW+RK>}m|5QdaKS-kb3weL(Y)XO+mPe86AcWpn$w>ut9#t=w7a5iq=++dNG9`?LFU zehbY%CuIO4FnVu>?K{+!oarA!!82P~P-*Jiv5?XkS@8BJD>5WL_do!JTZ&%?2ciBN zsaa)8jj(U?y07FwO%oNs7BpJ1UVVXXjXoff33O!4dX9E3{U3WtjPhj-gH)WAr0hnP zlMr^#iA%HC#G027%I7XfBuXoap9iOKLG@Fac7%Z+`tm~4x@_gpnx9y?kmJyuA9KE> zx-=k1osTpR=Yime=eY9SpuW?nqiVSj@*&x{( z;@XlPBezhS@OmxM!U2SPG1>A-At2NJ^vmU4;UZ;cgjUBL|KXYyPgVySd93M@>{k2E zmg|UM)ULCwl`jg=Ah}z+7Omya#sBTNXjn%c1IOHO5@`qO7X^W!{Pj+`8OrYzMHZ&# zx1^81{9%nv1eB;6@zpEfbeCr8Su5}Yk9gXbjRJ#@A}OvUN84zTBpl<1e@zh38iwkr z!bZC@xBrCHu<4s~fE;J!?;IU0cgQY|s_aR#&1Sr%l2Qb?_%Yn=v7&uJzs*XZz9i+5 z@J~trgS@R5_IUZYDM*ald{4j7qfx9z35+W6)e>c8IU}ITBtIZh%}>oOI+g;#9NY`_ zRs(`i5avPxFu1m%;9J*>+NI#Q$tCQ?Bdo?XA??F34AJ>Rcy!@ROijsKj~ug`GzAv9 zV@S?evJF#KLKC*-Czz)8!F{$~453#VO?zvNF}(o@3;j-*NJH7t&C)nNb8Bdc*g%W1 z=v@2#K1>A-3wjPK#yl2m<8*Vse(fMW{fk|F2bm1S^63t!FT%&t4f3Il5!P;e*K=gn z@tza7rP9(qeI)`xG44AP zAuN6tF0i(}4IP2m#*Pw8Z+ZQ*zP&w}v|@>0&y7xiNli?Nf7Eid=$b?R4dQcQMpZmc zAT!P3IKyFvU7w@IN?`UK^B>3h6!q8wtF6a~4CH4%@n=}ig^9v#@l3_2oYp|Bwi4j} zO-TPDX6CSqwN%8Zq$a8C{`-}?TW@dF!lA59-{uR$!z;|=?8Ux_*Wf;=jickn= z@N39r9a4Bmk!`Kz`cVand&q!qFnS%VT6oMJHT!Qgj8}2=sYlPjMreN6^GV`_0;+Ee zCzsr+O-8bQt|W9z@4myMB3E%(HzdFjNSFIpf5rR8XgP{(9+4v88>Q2LvVe}%fQr=k zlKl3EV+NYPw;Wvb-xB&}bdg__`OF^%m*>YJo#MAsXukb*kIge^*r`*$QhA9|y5!|8 zIUWIy^wc-5CFBebf=eB*WYu}XlREn$4FCfY=!gB6HkTuwNaM}_a=KZcr<@%GZ`4oRkvmbV+g#5{D>DsnSLu-C=haWh;dkgafnGC6xe`q*!MU^ z?~+CLuj1>!Lw=+0@41+nEYqO|T|aV#c)zI?I-z$W!OZjkFulP(sK@}jVRerIfML&f z^ib*?uYsvlTkkd_^Vehb*Qf34;jo+_K{SMq#o{ z((b0f#T?`iF(&MhLo~dJWfT6H5FaK&6mvdjUP5@}w6OfIzl#wOlnTUZ^{L8BH($CLM|k&82JZONcLUD@Kv9etm4HldKK zHf_Dh?!;eY&h86 zg1DvXi9TbA+tPJ~(M~kOxOGw+Y&f>wGSGay$#sR~$-Hh9$+gi!xEX)t>51tVvNyYL z+R^y3RcFuBlZEI>cst&HXLMB{nB#C_w&j_>u>C6d4EDne^^2v=rS-(XD z6?zw1#&`r}K4)bJgnZhUGR~$`LzyCroY_;WB$r2_Y;pkdKYmXg4e{q{DmCB8V(`uQ zPW!?XG*^VG*y!KUi{NP_A7A+1J#a1N6p;P7^FkIu2da{~k7)j(3S_5Yp4hpr4^@O7 zn-Ga8`^36B!kl8wE@1eGG2sPINm61H*p$E;65MmdS6n~NSAr5jo`}Ny@muo3JrIT} zDv?ZQC4wHH#@fUV1BJ=hD*!B@#uVul*wLdJ6v*Or^Ql;7DY}gK>jqMX+|Qd?fK2QJ zQ{Lz_K%RK`yJbxDVrl&w*EmnbK3Ff1d;qR#7!CRFkeN&PF9@T#V1-bkBo7>mOXVRg zF9d>t*$7WQ@e!>2QL%AHEWMEYxt14DSpKh~@+UWdU@L*(Bdg+FQykV!2&9)-;;%zI z-O-SaU;hfok#A{yS5`wkZcu%%rRSpToZwLR$i!BDnV?N}jjHAVmju{0hDVl7Gk$GKcukwgd?KZqUnBhLI|2-SAT`M6!VfIF^i4kz zOf-}OL@qSH`GrO@?FHWd@mb>i(*5i0_k864>;WSBRd;naMF+B3ELe~pV-dyFHEz)I z7@@T_3f>C5Q_inIJpBY-^G%uiDYI{710_M9fRhdD$J-4=8))wLgY$4s#3?5(aNVuI z{`hZWOpN`@Gg&n zYeD-9dI^k&8{LXc-{JtO^sFA4jNoL<+km`;Z;3KESiQB4I7$5l~vibXjZcnfFPf8 zX9e+D9;sI;!V#NY?)2Wgr=mk#q?U;5T9I*=bl;r5Kx3O7pL6jaVK?WT@Ar(V$Mx>D zah}Dx{w?C&0;PWmE8Ts+R9AU^=cy*t;I^wTptT1^pwPRnJ<IJ8PFqsgVL zcAYWDM0hEyHMhbd!MXTzQ2os@2Cxo{iOpH(WG_974_gPu8X*$LlWVDCM*j7Q%?8og zu5w5zxeBa)=KYh<_*-iPna@&{9D?6O<(B@s|Bo|SPVYr~WBJZ-c0HWM<&E|UuX^hyRJTd2>Ie;|RIHMS1VB6U`vvSerNY*cZ$!NT4WeAXax|peSMh_UTiCHv({V zc#*}eve4?|TRykK@pc_k0SGmT*Ul z5_fH3z_0Dr1jW}FKbJAP3rmB zhE&L~tBGjn9YcP)ZtA9>(7I7EzqX5dey@H7?-Ut)nM=m<%8@ggyn*i$B&Qeh&-$)E zOqVD7+<)4?PDstcPfkg1K2>wI(xbL=MB5SD;) zoO5mRbF12|P09YsbBmCJ2w?*|CQ0y!(D|VOPh#`-rirZ!CxGy_QH4<7aV}wMUN)|o z&4+Yr{#0%8FY3{YN2|{$ibWDw@M!Y()U-nnCcE=V9$d#gdb-=2I z8rA1>u=tJpY%BP%`B|}Mja%F7ptWhO9il!N;iuD&1%UH~;jJmMtHOp}#1L3nh6&7+ zKo`;o!O|SUfEA+M_WZh97hDU+q^EuU1+MPSCJZsM$9?AA@22gHKT@QLP|PwKGZMZ_ z6E!jdj`Fz&$B8@B-&ct(xB($TePV*gAc?gWwS61GfIw1HdL5dt`*j4g5RR|Y5@Y-Q zuUvocPe3ZvfKEM)?u>*+_gjw;Lf;qs!ugYp@5rw$K2sxufI9^TV2g<1Nvym z9Oz1(JWjq%MMpR6t*|Hg0MH#%%A6WwyrVmB9c-ZLfkc*5j2~n77d9)l%G{) zvC~+^nIe}=p=Z&+E71TU5@Q8N)-DjaG|FKdV}L2=WM1fcw2EP^*AE^Sf?ay$$m`$# zPZ?JN57pcKXXcWuV=Mbk$eyx|U6kzcBgqyKKYNy}V=0lHawUwiLY>+j&O=r?9Z(5I9cpF z(eSLzIArJ3vrfaz{I6R^ZK~AbPvu1q;rghvgN(CsA6SPl7-M$DrNSRf2#rUMmDKnH zjdv^g4RbvV)<+M{ta3$^-!5{xlrzGfWjbEuiN~F3!rcussjUbVS@Fr>@wmOez$CRy-%@&n3)8d^wyKH4Gldq=2YYoCs>+#qRT3g{3VRU`*ChuvN z#kUTt3mp$L4x2Ja7uY$Wmr&l{-+bujl+6*VFq7-;*o$)->9gnA+?bkE3);N=uA^uC zW71#k6AEIt;MkPPvf_P^^p4j4qSJcXz?)H{BX}LIP>eXKV}iaaqDVJSm!{qf7?0#7rGW_Erlb>!OR;Bo!dC>k=BURpz%c6+7=bA4=RkQi0 zSekv=;f!##k8biOx_YpKLUe9xCk|KH)7d>)%6wfmD>HX@cJoctNZPVSic({wJZ>aI z9WVcOwDHaX#Xv&!?531W@+mHQK7aY1K;vKBT;YSA6AM8apMNmp$NWwf5_&6=yT;H` z%Fx>f^u4V@3+RB$$9W80uxCSU1a)V|iPVFSPkDPfj=dS1YRQ$->VJNLfbSAy7E>JV zw^zeN-cT>*A$1bnL>c8o6l6yfehIYz4)}aIMm_czheSE{p-h2P??Nr6%nwfwVBy%KZF;3>=buY)(@z#Y-|czWqpoK+7^)x|+7F7)sIlp_CSIole1eaAf%N2(fbT~n_YrdQ-^2XPEJ2>#N z6G|)}*w*k#Y_qFFy; zoURT`E#tOqJGpbOp5v*U^IWY_ooEmW#;@{8eOj-nTo>wP;IP8TmBms_dna27=6>rv z-AOU$*(k;L;myvg-63fPK1$z>GgC|Yo1ef$8>VZ`LJY)3CI;wNHFxeZ(zk$xcMC&B z$_vtKc=Ot?+s)M`l*Ec%{lun9ZlO&Jcl);PE;9T4ZtDow-{70nwZP$7SmOoUyRHx`@3cT{rTX3CU(k7gtp@TN{ zd;^6}lA=nIwbuCHin`AAw~qb7ge}>t9Y!_j7>=}8^2ADSVig~|bAsgj$o_wVAKs5s zlbpWbJR@<@x%|9;FnugnInLQ57XqkO;Dsn@`48g&^FDZk~NyjB`bM0#1+m=;%aw5OE0;mY?>CmgM z-ZfWRq2k*;*R;dmsRe8c`qPA8rU09&>#7YemENNT1(UyVV)+etk7ugRIOv+-Zy!** zNjhV2Vc_8g-|ndt=nBwYs5%o>7u3c^nJAUK9vhyW>a*4Saq{BDON_Mok8d4#u5pEU z^T4~P%afnu_7x{pmCA}ZW<_*24?Ui}cTAWeJVx8#xOKbtCu&v&!1e9r_0%lM9z~q& z;yhy%D*H&namM9vFs_y%zE<5uFake^dD=(ow4NfpFwq{fk8n>eNypH3K$YszuX9w{ zzK8Q7>l5^TxEf+5pRrwhR;N;PkTQ5eg7(&g`K{{*gcTQeWV_Y0&qmCiY;zFuS4@sR zI?YaXs4i1gcAv|4qFf#mF0Yg`(bdwDk!r;?=U_kmwqWg&#N?y2+Mb@pBb6A5UdgUHZ;X|G0L>&tjNj?w-&lp-dS7(0 zuClmk(tNwnMfA2nAeQtKqE`b}At(R;x2e zEU|6Ux5F>X%R=i;1?FWE=YDqd8NBDiYN2k;lc#IEH3e2iDhjyMDTm)~UVD(p_&WDS z)XWA=gnI9%5}kc#gtB_MHqmdBhED4oV(}p6s967O!^%2))LP7Zs;{=_9e(hY!=oe8 z&@kV;l4@!87T|Z&!`S&nj!BjL;&gXa&Y_R~l?ka|MEY6qo16v$@6Nc$JTsre7gXAR zUOwXCeq

Q72T@1}`>t^#e)V&PjuL@uILl-E~E_cHF*p9B(@g-Hu~w$8ogdDBE!X zyNuZV%qgyg&5UH|DW926oVPq_^t^V0N5>s| z4QouD@=i@qX^LgptKv2(kR5s(Og7BFHHx|QJnaIWg(^sbQcFhko>$jMu8*Rbup3@}R)uKW z?OLK7T&(V*A{(mdLW9jys;N$4DT>57d1a|RvotU5kXHHE_r1MlduysNZ zt|?z76keju+*CX-R%JQSv-EjhVTykPPnD(A)2Mn{_m#5-JsXRh1Ger>6Am4hCHEg* zmo@0TcJ;H?cfKO3S0z+ZB~*{_B~-v(USzv0Tr-PL{)HD3<-cWy@!dI`dGCXn436JU* zG;;R(;6~Iw^HTDcvIa_QYl{xGFs@kB8~g942)TfcpO0)l?NkIrym;~>a#Uq<=1s7J zM{c>#SJfApi>IU}ccj0!cZFQZd1UU_83PhfY8&zWDTJBS0Q?hA^cQKd`5oy><981R zYzmb@ zhtgD!8oO^TkERJp{)J;2`lyJ+oL0<-rGkfX9MX$PhKswTmhe;T@~BI7 zSWZXDA?p@@Rs-!!=mTVlvtu@6n{R_NTfc2HcG0V`*I;AQ?8>>D?LrIZ(<{dN3PkQJNQ|=KpN)b2N#T^G z8@QZs+PVeY3#Z9e?KOb=dBl}t9&8@Y8Z8_PuA7lXRI@Tg5>{<})kUGjA(Xm2_QT#l zw~DxL@wdXZy7~-e@Wcox9?$iGnU7t3Ic-yK%H(bvK|5Z9UT3Uo^Ym1KELgFGJB2Uv z-%ps(xFo?BDz)Im*^=Q^TMktLIw?Bmm zaFsH;`YM1bw!$M(4QMpQ^>R4M_xI#SJ9CB9y<-XaLa%0YM9}u3itS5^HhCaK?&zhD z?LQ;96P?tvdZ&1#gJRW!avh(Jb4W`o2ASljZu3rk#`eeT)KVGaBNlj*HpG)RIFdK; zp|Vp-a&{@TXFfS>N8i7Fh2`P0d-RhelLjecJAO&2dpLpQCi=#?r^$4W>@T04kkA$p zjf)RycokyglKyn7Dlq>;AA9Q6)Uv{l2h?y|#2i;)@1M_~!?* zSDF}{mVyMsyN#uO3E}-(in8@<7z|H)MVCB{E6Uy}X$o&0J?7>;8lYG?&)c@tG&*+7 z+Hhdn_Wsz-Y123#-uUFpg}o(%xU>^C{AoKZAwy3cly+Q(6mDz0$a=J|=KT|uBX#7KbAFJ;CkE3$7LO%Hi zP0RBm>piE<``2IHu&;~HaU7I4Q*hNX`LgxxQn}CAN@D7(#Kxjki^A2Tg{%IBtBGp) z0Uvn>>zUltXx!8e1)g>ND)neSm9%*MuGRJxo-uJ!W>+7IR5jQi$yoJccZi{_~2>=@w8(gjS9=Uys4 z&$SJIt=fv;|91D&;h*P|4++~WmYFgCv|iz0Xz8CbGJJnmQ=REjyK3rZv=iZb=|i&@ zKM2?sJ+uACZ(IlDIC+ILr*d+={=J?X)}1@#K0 z|0dLznU4q!4|6VF3{bPQ-7PHZl%O+a6`h7`v!w}9yEe7CQoZITy(1>l)r50*3FpcP zTe_{aXTkCcn;jz$Qh*{X^W?V#-$h0U(q^9f~}_hM^}z_Aq zJ!HakT+I@^hQHqYoqyh9tC2mdL!{$E$AR~|_`d@iwV)Y`3DG?i$gst;*MwOBm=l$K z8bCOtTp|U+Nxka_AOvNxvN%F$_4Si=Sckd+26*gZ0O8lGFladBI|@+73?mDVU6ijf z9f`#tuExfGpb?_)Z4@iw!F(xz8|HxBbZ8@(Aac>8T@Y>J6)c(wNs5@og3hN$q&NYz zK9U6SnlRcI2~OM;Mb{yEAW;%+fv7P^p=T%&YV`=(iw2=w)X=PCUbr?|ffnJ_7@;%h z5X$f@+IJs9J##=$@*>kENqjxT0cbJu`%owgc)BFK94v7Gk6t5ZI2(;V$%Gim-au~% z!z&_6)S+|88NO^lzb0pR=Na0G%%f^U7m<18o#;Gf#Mbl;`UH8M*%Rn+vL5FYI`tsp z@6iT27r~!sw2gjFMj3By@y!ex&nd(Y_R(TwiAHFQKMkVYMvDnnM!ah3Vjfd7Kp9#6 zqPINr6pcci0>Bm{v@B?EhG7M_HqiTtr%W&-Xv9v~4D*nj*N_F~Gg2gCvn9qA;X(5{ zcMgeQ@S=|YB! z64!5Iu9Cgrb1?_p;dDTd3XnmFE-D}bAx`ZFkm06a)P4Y&ZVI;U2c%(F;L3hL1tH|1 z`K5?*AT?lwa5|^~dxUr=3~+%uG=MVl6P^ZeM~DR)zyYx^p#?k;P7^I~5h29sARRcD zOgi8ndKmmaBTm5$I>;?lYVa&Qz=a8p&iEiJnUTE2knuX23v}QGkQN8u!ori+Kmjzg zF+&8L@#F+d{k>_2udvqca)emG9TlFKU`1q#&;BpzTK!Q3t5X}ilf;U(oz+rAF%%!F$b1mu72P91m&=^(# zf|xvW*D9xD#i0mVWFLg!-h}+Hpa4H01v;<-NBH0ZeS33yM28*%TO5Vb{*yTjb}U%I z3YZ~EKU}t*$;sDgMa{nBJ3B6fjks{e@CGhQD3!+Bw?q@;Ln4<0k1xyo}@#GLiy97P_chh z;iR_^RRhq64bVrxYG8u`P9K3CsDS0rF%If;{Kiq09THin{SnFjZCkO?8!C+uo?Cn< zl--^H|7yefNC50#S9Tnbt3o4KWC8X;0T9@3QUZD7P?{H+P^geSUQ{wUfp{dpL{7-E z%mKEP%+M^R%ydL^APs{B!2&#BFe|`;tjmE15~V!*Q)n7+?HRPwdxfDA!kc_h+dM}CYEfUPMeX;*Uu`A( zgIb6mG~fjU5P#mhkiXncvgk|aZ;%ktdsw9YhAc{WgFf=N-Q4*gQQQO~`i(9N_;mt( z7*XMa6FC&>U7&Ihh023A3Iy1nM`6@{ke?qog)G#QAM$g1<4G%A`}{Q6oU3JblGx%eFDF&CSXw|EiBRiRp9d!-rgpH5La54Y!Ir8 zVFSwre;YKxq7TNfNE5k*LFg7{miX`O4=<^g@!-Xy5VlbewtG)5X3#+hx*XszeL|40 z77HZIA82HS0Rg0kI63-nEwk9qBhW|q_iX}SL})h9R0NRRlLLIN{Jq^#D1|?u z!duh0jdt5M_;HC()emG~ELr~7WZm>uKYJ4so)uc^f7m!qu3f+zTO^>qF$h!1Rw_ndqIef9{PQfz$*Vabj1GX!-tNf4-Abk z7$FMm1Eryt4M+xRqEH6@e?>e9HxK5P`h#g=fGjxwAKhOtcl|f_WB`Wue?Z#v`s7b^ zGDxXn&_8IxNS0;uDr2A}l|j$FAdLo*BWj2Pf=Jb}n8U|+AwTty>Zv{cRKZAbz#4fm zFfI1yswyu!GH#P|YACuvZdVgl8^Uj+Fw~ okeZ20LCt#=)Rp>uz_>v_14JqS9HWA+nrBh~<35G`P#2;852Q;6>Hq)$ delta 24254 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 From 443e8622cf45feb0c4a968a60db5e86dd04bce0d Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Wed, 6 Dec 2023 18:25:29 +0530 Subject: [PATCH 112/148] fix: core config type validation (#175) * fix: core config type validation * fix: pr comments * fix: user config mapper * fix: config mapper --- CHANGELOG.md | 4 + build.gradle | 2 +- .../supertokens/storage/postgresql/Start.java | 2 +- .../storage/postgresql/config/Config.java | 8 +- .../postgresql/utils/ConfigMapper.java | 148 ++++++++++++++++++ 5 files changed, 159 insertions(+), 5 deletions(-) create mode 100644 src/main/java/io/supertokens/storage/postgresql/utils/ConfigMapper.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 382fc935..41189171 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [5.0.6] - 2023-12-05 + +- Validates db config types in `canBeUsed` function + ## [5.0.5] - 2023-11-23 - Fixes call to `getPrimaryUserInfoForUserIds_Transaction` in `listPrimaryUsersByThirdPartyInfo_Transaction` diff --git a/build.gradle b/build.gradle index 894551fe..754f70d7 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'java-library' } -version = "5.0.5" +version = "5.0.6" repositories { mavenCentral() diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index b3ad7075..91940ee0 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -719,7 +719,7 @@ void handleKillSignalForWhenItHappens() { } @Override - public boolean canBeUsed(JsonObject configJson) { + public boolean canBeUsed(JsonObject configJson) throws InvalidConfigException { return Config.canBeUsed(configJson); } 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 20c55bef..41088d73 100644 --- a/src/main/java/io/supertokens/storage/postgresql/config/Config.java +++ b/src/main/java/io/supertokens/storage/postgresql/config/Config.java @@ -26,6 +26,7 @@ import io.supertokens.storage.postgresql.ResourceDistributor; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.output.Logging; +import io.supertokens.storage.postgresql.utils.ConfigMapper; import java.io.IOException; import java.util.HashSet; @@ -103,11 +104,12 @@ private PostgreSQLConfig loadPostgreSQLConfig(JsonObject configJson) throws IOEx return config; } - public static boolean canBeUsed(JsonObject configJson) { + public static boolean canBeUsed(JsonObject configJson) throws InvalidConfigException { try { - final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); - PostgreSQLConfig config = mapper.readValue(configJson.toString(), PostgreSQLConfig.class); + PostgreSQLConfig config = ConfigMapper.mapConfig(configJson, PostgreSQLConfig.class); return config.getConnectionURI() != null || config.getUser() != null || config.getPassword() != null; + } catch (InvalidConfigException e) { + throw e; } catch (Exception e) { return false; } diff --git a/src/main/java/io/supertokens/storage/postgresql/utils/ConfigMapper.java b/src/main/java/io/supertokens/storage/postgresql/utils/ConfigMapper.java new file mode 100644 index 00000000..dfd41e11 --- /dev/null +++ b/src/main/java/io/supertokens/storage/postgresql/utils/ConfigMapper.java @@ -0,0 +1,148 @@ +/* + * 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.utils; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import io.supertokens.pluginInterface.exceptions.InvalidConfigException; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.util.Map; + +public class ConfigMapper { + public static T mapConfig(JsonObject config, Class clazz) throws InvalidConfigException { + try { + T result = clazz.newInstance(); + for (Map.Entry entry : config.entrySet()) { + Field field = findField(clazz, entry.getKey()); + if (field != null) { + setValue(result, field, entry.getValue()); + } + } + return result; + } catch (InstantiationException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + private static Field findField(Class clazz, String key) { + Field[] fields = clazz.getDeclaredFields(); + + for (Field field : fields) { + if (field.getName().equals(key)) { + return field; + } + + // Check for JsonProperty annotation + JsonProperty jsonProperty = field.getAnnotation(JsonProperty.class); + if (jsonProperty != null && jsonProperty.value().equals(key)) { + return field; + } + + // Check for JsonAlias annotation + JsonAlias jsonAlias = field.getAnnotation(JsonAlias.class); + if (jsonAlias != null) { + for (String alias : jsonAlias.value()) { + if (alias.equals(key)) { + return field; + } + } + } + } + + return null; // Field not found + } + + private static void setValue(T object, Field field, JsonElement value) throws InvalidConfigException { + boolean foundAnnotation = false; + for (Annotation a : field.getAnnotations()) { + if (a.toString().contains("JsonProperty")) { + foundAnnotation = true; + break; + } + } + + if (!foundAnnotation) { + return; + } + + field.setAccessible(true); + Object convertedValue = convertJsonElementToTargetType(value, field.getType(), field.getName()); + if (convertedValue != null) { + try { + field.set(object, convertedValue); + } catch (IllegalAccessException e) { + throw new IllegalStateException("should never happen"); + } + } + } + + private static Object convertJsonElementToTargetType(JsonElement value, Class targetType, String fieldName) + throws InvalidConfigException { + // If the value is JsonNull, return null for any type + if (value instanceof JsonNull || value == null) { + return null; + } + + try { + if (targetType == String.class) { + return value.getAsString(); + } else if (targetType == Integer.class || targetType == int.class) { + if (value.getAsDouble() == (double) value.getAsInt()) { + return value.getAsInt(); + } + } else if (targetType == Long.class || targetType == long.class) { + if (value.getAsDouble() == (double) value.getAsLong()) { + return value.getAsLong(); + } + } else if (targetType == Double.class || targetType == double.class) { + return value.getAsDouble(); + } else if (targetType == Float.class || targetType == float.class) { + return value.getAsFloat(); + } else if (targetType == Boolean.class || targetType == boolean.class) { + // Handle boolean conversion from strings like "true", "false" + return handleBooleanConversion(value, fieldName); + } + } catch (NumberFormatException e) { + // do nothing, will fall into InvalidConfigException + } + + // Throw an exception for unsupported conversions + throw new InvalidConfigException("'" + fieldName + "' must be of type " + targetType.getSimpleName()); + } + + private static Object handleBooleanConversion(JsonElement value, String fieldName) throws InvalidConfigException { + // Handle boolean conversion from strings like "true", "false" + if (value.isJsonPrimitive() && value.getAsJsonPrimitive().isString()) { + String stringValue = value.getAsString().toLowerCase(); + if (stringValue.equals("true")) { + return true; + } else if (stringValue.equals("false")) { + return false; + } + } else if (value.isJsonPrimitive() && value.getAsJsonPrimitive().isBoolean()) { + return value.getAsBoolean(); + } + + // Throw an exception for unsupported conversions + throw new InvalidConfigException("'" + fieldName + "' must be of type boolean"); + } +} From cdebc2cb703204fd3bf036e7ba238b487176ccfc Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Wed, 6 Dec 2023 18:30:13 +0530 Subject: [PATCH 113/148] adding dev-v5.0.6 tag to this commit to ensure building --- ...-5.0.5.jar => postgresql-plugin-5.0.6.jar} | Bin 208337 -> 211778 bytes 1 file changed, 0 insertions(+), 0 deletions(-) rename jar/{postgresql-plugin-5.0.5.jar => postgresql-plugin-5.0.6.jar} (87%) diff --git a/jar/postgresql-plugin-5.0.5.jar b/jar/postgresql-plugin-5.0.6.jar similarity index 87% rename from jar/postgresql-plugin-5.0.5.jar rename to jar/postgresql-plugin-5.0.6.jar index 55a0fb605d46abe85d7f09693cbf2355b6299fc2..00284c3311fae1e5899b297d3ad7118c416e12a3 100644 GIT binary patch delta 20118 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

JAczR> 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 Date: Wed, 6 Dec 2023 20:16:02 +0530 Subject: [PATCH 114/148] fix: null handling (#176) --- .../postgresql/utils/ConfigMapper.java | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/utils/ConfigMapper.java b/src/main/java/io/supertokens/storage/postgresql/utils/ConfigMapper.java index dfd41e11..e9cd060b 100644 --- a/src/main/java/io/supertokens/storage/postgresql/utils/ConfigMapper.java +++ b/src/main/java/io/supertokens/storage/postgresql/utils/ConfigMapper.java @@ -16,11 +16,11 @@ package io.supertokens.storage.postgresql.utils; -import com.fasterxml.jackson.annotation.JsonAlias; -import com.fasterxml.jackson.annotation.JsonProperty; import com.google.gson.JsonElement; import com.google.gson.JsonNull; import com.google.gson.JsonObject; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonAlias; import io.supertokens.pluginInterface.exceptions.InvalidConfigException; import java.lang.annotation.Annotation; @@ -72,21 +72,9 @@ private static Field findField(Class clazz, String key) { } private static void setValue(T object, Field field, JsonElement value) throws InvalidConfigException { - boolean foundAnnotation = false; - for (Annotation a : field.getAnnotations()) { - if (a.toString().contains("JsonProperty")) { - foundAnnotation = true; - break; - } - } - - if (!foundAnnotation) { - return; - } - field.setAccessible(true); Object convertedValue = convertJsonElementToTargetType(value, field.getType(), field.getName()); - if (convertedValue != null) { + if (convertedValue != null || isNullable(field.getType())) { try { field.set(object, convertedValue); } catch (IllegalAccessException e) { @@ -95,6 +83,10 @@ private static void setValue(T object, Field field, JsonElement value) throw } } + private static boolean isNullable(Class type) { + return !type.isPrimitive(); + } + private static Object convertJsonElementToTargetType(JsonElement value, Class targetType, String fieldName) throws InvalidConfigException { // If the value is JsonNull, return null for any type From 033c015d17793b79c0f033c13641b5fd238b122a Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Wed, 6 Dec 2023 20:23:10 +0530 Subject: [PATCH 115/148] adding dev-v5.0.6 tag to this commit to ensure building --- jar/postgresql-plugin-5.0.6.jar | Bin 211778 -> 211671 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/jar/postgresql-plugin-5.0.6.jar b/jar/postgresql-plugin-5.0.6.jar index 00284c3311fae1e5899b297d3ad7118c416e12a3..e1cc4cccec93082e06a684117033f53d3444bc35 100644 GIT binary patch delta 5038 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

F8pH|Ju*_r<`A;j^9?I7W9)tB(gfno2g zoo#%>zQrkxJ1WUt_Oa06xpk+Sm+i~~!jpNtMDf4q{UBM zjYMsFS3{DInX9JdqH$0%VTkAjt^53D=X^_KW-Z#S5z%IBak6`ecyiUmO2JsBS#()C zUY6~~CtrK>fn{o&Zt*?YV^018aVlxT)M-*;XI)eKnXKRwZ68Zii5@pqZdgU%-EzEn zvfyq4=1YKGe)?_6l47d$AF8bvdUa**)}8M-qjl9Sig#1N-(7!uGzp8+OPH4FYYsoGkV7A4F_X$`K=gc;_#--><^0TY%{XQBFXMJS2 zn_`9}Rx5?;O6)|UK~?LqLJ zFw4o~dWvOY>c`ZN1)ywn!nVF_-tjOLoEf9Yu~I_a8^{CuI?ftwkOj4TD6K2gkg40+ ziQ@q#UK}#>;ddG%6CZQ$GrWFC{v&?Pdo*0Ksqj?P6C=X0ifdUl1c-UW7HjOH{r5<{ zc0eTAXP>2Lunwfcm-bPK?@W;3iSB{Z`yJD`S~sF^CO>`EqQahwkgZ-+JE>wU|50OW z%Yx`jf5k_gG9Tp3?s;KZHT!U@S&4Gf(87u{6o__xLZvOS6_wYFSIged@a(A zyn1Xd;Q^IyWzpE8o8XhzmWiM0-S`y5=4{tZTFz7u9Nxv#*OVsoa4BM<_9r{^B!q60 zdjI#7n)U2Ln3XyD)Yj9cz4fEsaxdreARRAOmi1b-D{g0Cglf{L@8N@@^pvyzY-DK|L(kkv_jGS8h? z!JEpPsO<17me=I)#e3c7$GelJ8VwKU@5d$vCF(rkRalMba4_YVOX(*~i}h>)1t}`EC z8=B2d$}*NRAl(hUZ`5@ND|vFeQ(V=NZg^$O=I7k0Qp-M3)=XpK`c%5(_ixZf>XXZo zE(1&LIxX4D&r}H;xwenXc1@Ja4*U+YN!iLiy)}RB#(3P_v^ioAMI_Kh)lC zTUQt*_a;siO0-U@L>P1l#Z$`}tK~7%es)#r*N%R5F&w8d`>9rRG{ahvyq!Y=`L6f{ zqDvVZ>FLL+akWY#Xa4$3e8%C63N;;2MkiYQ+}uf-=W1kw>Rab37=J_4 z!PqkgkI!%aXWm7|Ab}7FN1Fhx!XQ&H7u^ekF3<)f#t6udX55T~&e4pSTaYUnOj9OA z$e%V=b;Ll`G{Zk0vZu{yFOwifn(-kOnqdRtXL-!^lt59mbWAD4>j9=H1#Vao z;8|{%0C1Wc27@=54+DeAo9u{z6#&L#V0C~!7+4p?S$SY{;GgD!odLG;z<7Xr_rjh4 zBlp5*fo^Uud>Qz5ys$gKXS_%p;pO>YJAhGqum|8*`F862@Wa=D|A`-t0r6}5-~@o% z``~bZ5dugY!F2(c2ryJ|CvRJDM;|K$hX9^Q7`_D7@fSwcNBob%uoFOSk)8S#B0D-c zQTPhrDWb@Fh3)I2s^y z5Kag2tb_0cfb??6xe<(#+c^ilJfcT@KY)mSPX2%Uu7K(zgkbnO}vXCdjAfbbnCTNHo{=b`}s1uytvJ%W_ z6si+>ENOQ}JFkMU3laanAPdofZbJO2&q9xLIv_1|0ii8^1qU_RA4ch zKAkgUbQ{qCGLa?M5Mfy>Q2O56^V$)?nIJR zVLl))1LT`{K&qx4hB-yRRams@KFw0(F6`^c)_Tg?#QXEJD+h;;v?= zvY}A5A}Ex~Kl&r29X%Tf;->Wh)SN$z)D+3ziO8tI2ZTY*2rLR*ci^^?qmIBP4976wChva$+g;;@ From 08043dc87569531c0756253b6bf6fa4b40254f5d Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Mon, 25 Dec 2023 14:35:34 +0530 Subject: [PATCH 116/148] 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 117/148] 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 4040aa2439c35cd5b5a62a66a68fffe4c7277ef4 Mon Sep 17 00:00:00 2001 From: Ankit Tiwari Date: Tue, 6 Feb 2024 11:02:45 +0530 Subject: [PATCH 118/148] 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 --- CHANGELOG.md | 4 + build.gradle | 2 +- .../postgresql/config/PostgreSQLConfig.java | 14 +- .../postgresql/output/CustomLayout.java | 4 +- .../storage/postgresql/output/Logging.java | 11 +- .../storage/postgresql/utils/Utils.java | 17 ++ .../storage/postgresql/test/LoggingTest.java | 274 ++++++++++++++++++ 7 files changed, 315 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41189171..7ab4f065 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [5.0.7] - 2024-01-25 + +- Fixes the issue where passwords were inadvertently logged in the logs. + ## [5.0.6] - 2023-12-05 - Validates db config types in `canBeUsed` function diff --git a/build.gradle b/build.gradle index 754f70d7..f98f1df7 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'java-library' } -version = "5.0.6" +version = "5.0.7" repositories { mavenCentral() 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..cbfac0ab 100644 --- a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java +++ b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java @@ -548,10 +548,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 91a58735..7e662f8f 100644 --- a/src/main/java/io/supertokens/storage/postgresql/utils/Utils.java +++ b/src/main/java/io/supertokens/storage/postgresql/utils/Utils.java @@ -19,6 +19,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) { @@ -53,4 +55,19 @@ public static String generateCommaSeperatedQuestionMarks(int size) { } return builder.toString(); } + + 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/LoggingTest.java b/src/test/java/io/supertokens/storage/postgresql/test/LoggingTest.java index b27c1ac5..6e2f792c 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; @@ -309,6 +312,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"); + Utils.setValueInConfig("postgresql_password", "db_password"); + + 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), + 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), + 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(); From 13753ddea071074870768f1f4da3555d7a679944 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Tue, 6 Feb 2024 17:15:23 +0530 Subject: [PATCH 119/148] 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 --- CHANGELOG.md | 1 + .../supertokens/storage/postgresql/Start.java | 15 ++ .../postgresql/test/DbConnectionPoolTest.java | 213 ++++++++++++++++++ 3 files changed, 229 insertions(+) create mode 100644 src/test/java/io/supertokens/storage/postgresql/test/DbConnectionPoolTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ab4f065..5f552711 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [5.0.7] - 2024-01-25 - Fixes the issue where passwords were inadvertently logged in the logs. +- Adds tests to check connection pool behaviour. ## [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 91940ee0..73bc1597 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -106,6 +106,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, @@ -2990,4 +2992,17 @@ public UserIdMapping[] getUserIdMapping_Transaction(TransactionConnection con, A 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/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..6917e01e --- /dev/null +++ b/src/test/java/io/supertokens/storage/postgresql/test/DbConnectionPoolTest.java @@ -0,0 +1,213 @@ +/* + * 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 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.*; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.storage.postgresql.Start; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.thirdparty.ThirdParty; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +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 static org.junit.Assert.*; + +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), + 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), + 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 = {"../"}; + + 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), + config + ), false); + + Thread.sleep(3000); // 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")) { + 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); + } + }); + } + + // 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), + 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); + assertTrue(successAfterErrorTime.get() - firstErrorTime.get() > 0); + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } +} From 0377d03c5e73f0af3737260e7aad45c2a2ce2af1 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 6 Feb 2024 17:57:14 +0530 Subject: [PATCH 120/148] adding dev-v5.0.7 tag to this commit to ensure building --- ...-5.0.6.jar => postgresql-plugin-5.0.7.jar} | Bin 211671 -> 213155 bytes 1 file changed, 0 insertions(+), 0 deletions(-) rename jar/{postgresql-plugin-5.0.6.jar => postgresql-plugin-5.0.7.jar} (76%) diff --git a/jar/postgresql-plugin-5.0.6.jar b/jar/postgresql-plugin-5.0.7.jar similarity index 76% rename from jar/postgresql-plugin-5.0.6.jar rename to jar/postgresql-plugin-5.0.7.jar index e1cc4cccec93082e06a684117033f53d3444bc35..5b50a8447c8cf10be955747fd2a258ee9837b2b3 100644 GIT binary patch delta 44556 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 delta 43129 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 QhDeYF6JC1*uz^7T2cR;)5dZ)H From 53b83c99a868db4f9951438a41e4cdcf4cc5d49b Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Thu, 8 Feb 2024 12:36:45 +0530 Subject: [PATCH 121/148] fix: cicd tests (#185) --- .../postgresql/test/DbConnectionPoolTest.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) 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 6917e01e..4bdbc707 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/DbConnectionPoolTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/DbConnectionPoolTest.java @@ -138,7 +138,7 @@ public void testDownTimeWhenChangingConnectionPoolSize() throws Exception { config ), false); - Thread.sleep(3000); // let the new tenant be ready + Thread.sleep(5000); // let the new tenant be ready assertEquals(300, start.getDbActivityCount("st1")); @@ -157,7 +157,7 @@ public void testDownTimeWhenChangingConnectionPoolSize() throws Exception { successAfterErrorTime.set(System.currentTimeMillis()); } } catch (StorageQueryException e) { - if (e.getMessage().contains("Connection is closed")) { + if (e.getMessage().contains("Connection is closed") || e.getMessage().contains("has been closed")) { if (firstErrorTime.get() == -1) { firstErrorTime.set(System.currentTimeMillis()); } @@ -174,6 +174,15 @@ public void testDownTimeWhenChangingConnectionPoolSize() throws Exception { } 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; + } } }); } @@ -210,4 +219,4 @@ public void testDownTimeWhenChangingConnectionPoolSize() throws Exception { process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } -} +} \ No newline at end of file From f29d7c18b2831065d0b6444bd49bdde5021c1f54 Mon Sep 17 00:00:00 2001 From: Ankit Tiwari Date: Thu, 8 Feb 2024 14:11:09 +0530 Subject: [PATCH 122/148] fix: logging test (#187) --- .../java/io/supertokens/storage/postgresql/test/LoggingTest.java | 1 - 1 file changed, 1 deletion(-) 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 6e2f792c..1e027162 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/LoggingTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/LoggingTest.java @@ -387,7 +387,6 @@ public void testDBPasswordMasking() throws Exception { Utils.setValueInConfig("info_log_path", "null"); Utils.setValueInConfig("error_log_path", "null"); - Utils.setValueInConfig("postgresql_password", "db_password"); System.setOut(new PrintStream(stdOutput)); System.setErr(new PrintStream(errorOutput)); From 5f2dc1d357bb3e84ac12e8b55912766dab1f021f Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Thu, 8 Feb 2024 14:30:15 +0530 Subject: [PATCH 123/148] adding dev-v5.0.7 tag to this commit to ensure building --- jar/postgresql-plugin-5.0.7.jar | Bin 213155 -> 213155 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/jar/postgresql-plugin-5.0.7.jar b/jar/postgresql-plugin-5.0.7.jar index 5b50a8447c8cf10be955747fd2a258ee9837b2b3..088261dfb046472cb5de46843064fe871d0f9f5a 100644 GIT binary patch delta 1739 zcmYL|dr(w$6vyxHcVHuAA%r4!c|TU3%kmac!Gw~dbt1rY>aA6GQxf#F@6<-ur9 zJY1Qxvzow?p2c*w(;Kucj|FNrR{_gf0o&poSiB6WP%zn<|G|BQ8Y=nG;Bw)1Sykn&# zP1$9C&TY!=uh>f~k$U|tR-}#L=pD9~P{j8xJN5*;Cm*o)aKFmBhwOru)xh~V?QkFa z@Yi#o%h?bPWrN!>Ym?PhO>#&!1u5ZG zd>LV4;Wi*=T@7#7#^Bbc-5gZb$Hz4CIL0fr$!vG?M>J{}GZ9`v z`BxL+9hBqDWEB&o#J9|}8J5HldLGahLKT2tLTCq|ERnFdHO||gn|cIrIv^JD0WtnsTWcFr7=U0oJXd4#3nJJz?rvYD2i?MJ{zdHTjh1#(pY delta 1739 zcmY+DdrXyO9LL}1`N4^h10fV~4)@EE>)~)0^uRF$GK?f}fGmqHTm(Ej7DnL(LJ6IC z*%`cRl+ze&UYZ!RH`e(Oo$}H_4X@*w?leOX)MA5Q2%suF|6#xpi!c1fk0yQR4aI4zWjeSorI$i@G^4Qs zu)Akk_?Gv@1^IhiYP^SpuZ;oohbI0*1B4(gPVJ*|Tl;LjT1t#v%Y0#QUe5X_DlVNHu+ z>yT9IjAt%Ee)`=Nt3z=-I zFEVZ_V8KFj6|tOU&`y>!w;#w+$6N-`L_NC|0UGRL4pI519`c5m?s}$pQa#qi(zTh-S%Pp``htxR1|2k7b=I+5QKsx_Am=vi=GW|{ zT2<`W0 zxK#}J@n`vI5piK7Uts9N6go4o=xOz$!R?r_sh(C%YFI9XXpuF131Rxel^|zB9q$lh zns4OmMJ8Jd?-u<`I5`&qlP+G20Je=ciGbE!+=;oWH}~)|7*qE025g5m(97Ee8T)v> zAlE+rvY>1G`F7ENML!=C?GMA6?i{gq+r}BQ20DCkY08B?yH(*1I^f?zpy->86sqf@c zJ?#JH$_sI>l$EcoBo|EU^QaQ=ejdF7@Xe>qfR=o!1zgCd*8yu*Q72$(l}wnvn%be9 tSS=N^oi;(~u*+KWc6n(P1+t%y3gpH`6;cPBhYG0%@Mj^}0XYt8`wxjV7~cQ@ From f540b86874c54bfa1538eba0a19f2cb58c2f1f1b Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Thu, 8 Feb 2024 19:14:21 +0530 Subject: [PATCH 124/148] fix: flaky test (#188) --- .../postgresql/test/DbConnectionPoolTest.java | 193 ++++++++++-------- 1 file changed, 103 insertions(+), 90 deletions(-) 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 4bdbc707..18d123d5 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/DbConnectionPoolTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/DbConnectionPoolTest.java @@ -114,109 +114,122 @@ public void testActiveConnectionsWithTenants() throws Exception { public void testDownTimeWhenChangingConnectionPoolSize() 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); - 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), - 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()); + 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), + 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); } - } else { + } catch (EmailChangeNotAllowedException 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); - } catch (IllegalStateException e) { - if (e.getMessage().contains("Please call initPool before getConnection")) { - if (firstErrorTime.get() == -1) { - firstErrorTime.set(System.currentTimeMillis()); - } - } else { + } catch (TenantOrAppNotFoundException e) { + errorCount.incrementAndGet(); + throw new RuntimeException(e); + } catch (BadPermissionException e) { errorCount.incrementAndGet(); - throw e; + 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); + // 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), - config - ), false); + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + new TenantIdentifier(null, null, "t1"), + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + config + ), false); - Thread.sleep(3000); // let the new tenant be ready + Thread.sleep(3000); // let the new tenant be ready - es.shutdown(); - es.awaitTermination(2, TimeUnit.MINUTES); + es.shutdown(); + es.awaitTermination(2, TimeUnit.MINUTES); - assertEquals(0, errorCount.get()); + assertEquals(0, errorCount.get()); - assertEquals(200, start.getDbActivityCount("st1")); + assertEquals(200, start.getDbActivityCount("st1")); - // delete tenant - Multitenancy.deleteTenant(new TenantIdentifier(null, null, "t1"), process.getProcess()); - Thread.sleep(3000); // let the tenant be deleted + // delete tenant + Multitenancy.deleteTenant(new TenantIdentifier(null, null, "t1"), process.getProcess()); + Thread.sleep(3000); // let the tenant be deleted - assertEquals(0, start.getDbActivityCount("st1")); + assertEquals(0, start.getDbActivityCount("st1")); - System.out.println(successAfterErrorTime.get() - firstErrorTime.get() + "ms"); - assertTrue(successAfterErrorTime.get() - firstErrorTime.get() < 250); - assertTrue(successAfterErrorTime.get() - firstErrorTime.get() > 0); - process.kill(); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + 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 } } \ No newline at end of file From abe531269fef0f742a780e0a7282ed9046f4a309 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Thu, 8 Feb 2024 19:14:55 +0530 Subject: [PATCH 125/148] adding dev-v5.0.7 tag to this commit to ensure building --- jar/postgresql-plugin-5.0.7.jar | Bin 213155 -> 213155 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/jar/postgresql-plugin-5.0.7.jar b/jar/postgresql-plugin-5.0.7.jar index 088261dfb046472cb5de46843064fe871d0f9f5a..3af418895e8c5f597a53f0fd448099410197678a 100644 GIT binary patch delta 1527 zcmY+De@xV69LMi@K60YmkpjgyjvoTYPmen|4&?VKj!;60luoZ!>J(25hzgR7_@Rb6 zHr;W3HK&8IZsfG2YkirUG|gQ8s9?!xR~F4|u0L8f-9lUI`+gq(>9+g6K0n^i=lMLJ z=ldP)HIDWg4fVCOV6jdY9$D%oL;I2T_qN(d_doduR>*HU zwWlwFxikdp*qH?y6|;vI_-y)U#8&H#vEXwFrHjBL=@HNbPiB7`2DaEYGmXA$c{Olj z@^?N9cK3fRWOW0U=twjacEW29X&pVU_x&m!6G_rLX)yPQJ1fklwghipUdN_uS7-8h5 zu(hbAc3D`vF#gD7=b=$fTiHfrST4JRQ>k|ytaUMrO%?1(5oxPpMG=U6xQ-1OU{rco zyB8jQsA?4(FtxWHz}h>7_3-tyenCUROC?qPZA z)J2vl6b~=60EwAJ2R4kmqI9~?%ax^^+tr`GV%wrnt?CC>BMvwCGwT+MjJMg*r;r++ zXQxEU^)LHEq{28qC(fc8cvmqrj%q%R%T#|`&uwB_u?_sJ5MSBIe`jKIQ{0sQp4Q$~ zz0WmeSG4v_in)kl)g(86ghhBwo<^1J9==CRWUh(Vi7K{c-Y1#}wsI2MiC1~Grv0p) zdxf;Qo3~+j%B_9;85GIc&zo?w>d*n+Aq>L+-zto@gM7O%W)JaB(Qf@99~TB4;cl_x zi#{GMjHdVauTP*(U4UN`Hy3w?9}vq&&T{u|?D;5;?4Z>|?|>hf2%mv+$3*xJl#FK)20dZrx+`lVb>gLm_1#xeNtoK(2^}>H6QyZ$rg;}%>+?_?AfXUg^4Gw41 z5LlQ)hrmlYv=6LVP66=G<@7qZ!%98iU90S;F;~vx%UpU3@l`gNzx#dc6s>b zm19M;75t}2&bMWS+-G(LwIg0zOdG(FV%h;NDxsIaT_v;`yi!7Mg05293r?2GI`%T^ vfIn6yeN#Dk;CGhG`g7%S{`D1d|4%FAdy-U1d!T#2k~V=4Dyaf=x~SzpvTj-w delta 1527 zcmYL|e@xV69LMi@K5(LNq(Cu_<3Ql}>2U|gaq{~VM<{_rO6b)}o#F`rK|zucKh#ji zCIkAqoDRmik<*f{^<{3-G{gK+!IIG^i)J?0A1#}1p{@12pKn{Y-S_qR@qRwf=lML} z?^w5CtlJP$RZambbh@A*-PP8CkYp~N&T`9iYgw&M{5T%sN8;~ovXSn8`E={R`&KTV zx13_pU@&tF$6vwD1_&x956>R7>BAA*pf^N<&&A{~7gGQ6q+n?Lr_#O&1RL$!8A@Ew zEJHdXYwxoNclLf20KcNo5*~_%0#ADF3!tx~YdfCQRh$IZNr9{!EYQDfu$<)P*Y>o_9#YBb=-1a;W z6FH-dbg@iz<`PShibq$Nk3yjc_idSQhv{^m6)LMZx2r#X$##XIY{~bmOdhWPC)P=* zZ@9w-o&)5~wo>4KmC{m5B<&UulugTM>vfIP=%ZV)3@d{bRR?mB66aFSn zQakx7FBRHPTew$B>pOWfhNs*<$e%%x^h3N3H>(aE=55jl>Ek=4(R_sOW;oLPQQppB zRQ2--Y0zBz5SWHEr(@l;40#3j*3IG5A From 5e27c9e7a839000dec770ee827e8d92eaf4c47b9 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Fri, 9 Feb 2024 12:43:01 +0530 Subject: [PATCH 126/148] 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 --- CHANGELOG.md | 1 + config.yaml | 8 + devConfig.yaml | 8 + .../storage/postgresql/ConnectionPool.java | 2 + .../supertokens/storage/postgresql/Start.java | 3 +- .../postgresql/config/PostgreSQLConfig.java | 27 +++ .../postgresql/test/DbConnectionPoolTest.java | 196 ++++++++++++++++-- .../test/SuperTokensSaaSSecretTest.java | 6 +- 8 files changed, 233 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f552711..5ebae7eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - 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 diff --git a/config.yaml b/config.yaml index 36459b8d..81032770 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: 1) integer value. Minimum number of idle connections to be kept +# active. +# postgresql_minimum_idle_connections: diff --git a/devConfig.yaml b/devConfig.yaml index 39d0d5ed..3af330ff 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: 1) integer value. Minimum number of idle connections to be kept +# active. +# postgresql_minimum_idle_connections: diff --git a/src/main/java/io/supertokens/storage/postgresql/ConnectionPool.java b/src/main/java/io/supertokens/storage/postgresql/ConnectionPool.java index f7dbc287..4f5cc53a 100644 --- a/src/main/java/io/supertokens/storage/postgresql/ConnectionPool.java +++ b/src/main/java/io/supertokens/storage/postgresql/ConnectionPool.java @@ -81,6 +81,8 @@ private synchronized void initialiseHikariDataSource() throws SQLException { } config.setMaximumPoolSize(userConfig.getConnectionPoolSize()); config.setConnectionTimeout(5000); + config.setIdleTimeout(userConfig.getIdleConnectionTimeout()); + config.setMinimumIdle(userConfig.getMinimumIdleConnections()); 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 73bc1597..86a7e876 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -117,7 +117,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(); 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 cbfac0ab..1e57cc2a 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 int postgresql_minimum_idle_connections = 1; + @IgnoreForAnnotationCheck boolean isValidAndNormalised = false; @@ -234,6 +242,14 @@ public String getThirdPartyUsersTable() { return postgresql_thirdparty_users_table_name; } + public long getIdleConnectionTimeout() { + return postgresql_idle_connection_timeout; + } + + public int getMinimumIdleConnections() { + return postgresql_minimum_idle_connections; + } + public String getThirdPartyUserToTenantTable() { return addSchemaAndPrefixToTableName("thirdparty_user_to_tenant"); } @@ -340,6 +356,17 @@ public void validateAndNormalise() throws InvalidConfigException { "'postgresql_connection_pool_size' in the config.yaml file must be > 0"); } + if (postgresql_minimum_idle_connections <= 0) { + throw new InvalidConfigException( + "'postgresql_minimum_idle_connections' must be a positive value"); + } + + 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 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 18d123d5..94434670 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/DbConnectionPoolTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/DbConnectionPoolTest.java @@ -16,7 +16,23 @@ 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; @@ -24,24 +40,10 @@ import io.supertokens.multitenancy.Multitenancy; import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; -import io.supertokens.pluginInterface.multitenancy.*; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.storage.postgresql.Start; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.thirdparty.ThirdParty; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TestRule; - -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 static org.junit.Assert.*; public class DbConnectionPoolTest { @Rule @@ -64,6 +66,7 @@ public void testActiveConnectionsWithTenants() throws Exception { 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_minimum_idle_connections", "10"); process.startProcess(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -87,6 +90,7 @@ public void testActiveConnectionsWithTenants() throws Exception { // change connection pool size config.addProperty("postgresql_connection_pool_size", 20); + config.addProperty("postgresql_minimum_idle_connections", 20); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( new TenantIdentifier(null, null, "t1"), @@ -119,6 +123,7 @@ public void testDownTimeWhenChangingConnectionPoolSize() throws Exception { FeatureFlagTestContent.getInstance(process.getProcess()) .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY}); process.startProcess(); + Utils.setValueInConfig("postgresql_minimum_idle_connections", "10"); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); Start start = (Start) StorageLayer.getBaseStorage(process.getProcess()); @@ -127,6 +132,7 @@ public void testDownTimeWhenChangingConnectionPoolSize() throws Exception { JsonObject config = new JsonObject(); start.modifyConfigToAddANewUserPoolForTesting(config, 1); config.addProperty("postgresql_connection_pool_size", 300); + config.addProperty("postgresql_minimum_idle_connections", 300); AtomicLong firstErrorTime = new AtomicLong(-1); AtomicLong successAfterErrorTime = new AtomicLong(-1); AtomicInteger errorCount = new AtomicInteger(0); @@ -190,6 +196,7 @@ public void testDownTimeWhenChangingConnectionPoolSize() throws Exception { // change connection pool size config.addProperty("postgresql_connection_pool_size", 200); + config.addProperty("postgresql_minimum_idle_connections", 200); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( new TenantIdentifier(null, null, "t1"), @@ -232,4 +239,165 @@ public void testDownTimeWhenChangingConnectionPoolSize() throws Exception { 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}); + Utils.setValueInConfig("postgresql_minimum_idle_connections", "10"); + 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), + 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), + 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}); + Utils.setValueInConfig("postgresql_minimum_idle_connections", "10"); + 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), + 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/SuperTokensSaaSSecretTest.java b/src/test/java/io/supertokens/storage/postgresql/test/SuperTokensSaaSSecretTest.java index 17b6e437..6693acb1 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 29da546d91df9de711e683019a577ce32f6274b6 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Fri, 9 Feb 2024 12:44:06 +0530 Subject: [PATCH 127/148] adding dev-v5.0.7 tag to this commit to ensure building --- jar/postgresql-plugin-5.0.7.jar | Bin 213155 -> 213487 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/jar/postgresql-plugin-5.0.7.jar b/jar/postgresql-plugin-5.0.7.jar index 3af418895e8c5f597a53f0fd448099410197678a..e0fc1a16bf9e37cfba2f063b24b354ccba1d5783 100644 GIT binary patch delta 42189 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%0D;Ds@#N3*pc|=R8u~-fX=<_CAb(wPM6%m?* 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 g0jm+*qamE&pX6T_GR4d^@Ej%JI%X~nm>Px# From 8383fd66c2ea1cfa074eb242c9ce3e29a45e5a19 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Fri, 9 Feb 2024 22:59:41 +0530 Subject: [PATCH 128/148] fix: cicd (#189) * fix: cicd * fix: test --- config.yaml | 4 ++-- devConfig.yaml | 4 ++-- .../storage/postgresql/ConnectionPool.java | 4 +++- .../postgresql/config/PostgreSQLConfig.java | 22 ++++++++++--------- .../postgresql/test/DbConnectionPoolTest.java | 7 ------ 5 files changed, 19 insertions(+), 22 deletions(-) diff --git a/config.yaml b/config.yaml index 81032770..38ade78f 100644 --- a/config.yaml +++ b/config.yaml @@ -72,6 +72,6 @@ postgresql_config_version: 0 # to be closed. # postgresql_idle_connection_timeout: -# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: 1) integer value. Minimum number of idle connections to be kept -# active. +# (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 3af330ff..a25dba97 100644 --- a/devConfig.yaml +++ b/devConfig.yaml @@ -74,6 +74,6 @@ postgresql_password: "root" # to be closed. # postgresql_idle_connection_timeout: -# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: 1) integer value. Minimum number of idle connections to be kept -# active. +# (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/src/main/java/io/supertokens/storage/postgresql/ConnectionPool.java b/src/main/java/io/supertokens/storage/postgresql/ConnectionPool.java index 4f5cc53a..d209411f 100644 --- a/src/main/java/io/supertokens/storage/postgresql/ConnectionPool.java +++ b/src/main/java/io/supertokens/storage/postgresql/ConnectionPool.java @@ -82,7 +82,9 @@ private synchronized void initialiseHikariDataSource() throws SQLException { config.setMaximumPoolSize(userConfig.getConnectionPoolSize()); config.setConnectionTimeout(5000); config.setIdleTimeout(userConfig.getIdleConnectionTimeout()); - config.setMinimumIdle(userConfig.getMinimumIdleConnections()); + if (userConfig.getMinimumIdleConnections() != null) { + config.setMinimumIdle(userConfig.getMinimumIdleConnections()); + } config.addDataSourceProperty("cachePrepStmts", "true"); config.addDataSourceProperty("prepStmtCacheSize", "250"); config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); 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 1e57cc2a..2c464849 100644 --- a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java +++ b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java @@ -118,7 +118,7 @@ public class PostgreSQLConfig { @JsonProperty @ConnectionPoolProperty - private int postgresql_minimum_idle_connections = 1; + private Integer postgresql_minimum_idle_connections = null; @IgnoreForAnnotationCheck boolean isValidAndNormalised = false; @@ -246,7 +246,7 @@ public long getIdleConnectionTimeout() { return postgresql_idle_connection_timeout; } - public int getMinimumIdleConnections() { + public Integer getMinimumIdleConnections() { return postgresql_minimum_idle_connections; } @@ -356,15 +356,17 @@ public void validateAndNormalise() throws InvalidConfigException { "'postgresql_connection_pool_size' in the config.yaml file must be > 0"); } - if (postgresql_minimum_idle_connections <= 0) { - throw new InvalidConfigException( - "'postgresql_minimum_idle_connections' must be a positive value"); - } + 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'"); + 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 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 94434670..cdf0c28c 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/DbConnectionPoolTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/DbConnectionPoolTest.java @@ -66,7 +66,6 @@ public void testActiveConnectionsWithTenants() throws Exception { 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_minimum_idle_connections", "10"); process.startProcess(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -90,7 +89,6 @@ public void testActiveConnectionsWithTenants() throws Exception { // change connection pool size config.addProperty("postgresql_connection_pool_size", 20); - config.addProperty("postgresql_minimum_idle_connections", 20); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( new TenantIdentifier(null, null, "t1"), @@ -123,7 +121,6 @@ public void testDownTimeWhenChangingConnectionPoolSize() throws Exception { FeatureFlagTestContent.getInstance(process.getProcess()) .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY}); process.startProcess(); - Utils.setValueInConfig("postgresql_minimum_idle_connections", "10"); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); Start start = (Start) StorageLayer.getBaseStorage(process.getProcess()); @@ -132,7 +129,6 @@ public void testDownTimeWhenChangingConnectionPoolSize() throws Exception { JsonObject config = new JsonObject(); start.modifyConfigToAddANewUserPoolForTesting(config, 1); config.addProperty("postgresql_connection_pool_size", 300); - config.addProperty("postgresql_minimum_idle_connections", 300); AtomicLong firstErrorTime = new AtomicLong(-1); AtomicLong successAfterErrorTime = new AtomicLong(-1); AtomicInteger errorCount = new AtomicInteger(0); @@ -196,7 +192,6 @@ public void testDownTimeWhenChangingConnectionPoolSize() throws Exception { // change connection pool size config.addProperty("postgresql_connection_pool_size", 200); - config.addProperty("postgresql_minimum_idle_connections", 200); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( new TenantIdentifier(null, null, "t1"), @@ -271,7 +266,6 @@ public void testMinimumIdleConnectionForTenants() throws Exception { 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_minimum_idle_connections", "10"); process.startProcess(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -326,7 +320,6 @@ public void testIdleConnectionTimeout() throws Exception { 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_minimum_idle_connections", "10"); process.startProcess(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); From 54b4f9ab5c55d1bfe6629dd2c4a27e10c1e76945 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Fri, 9 Feb 2024 23:10:04 +0530 Subject: [PATCH 129/148] adding dev-v5.0.7 tag to this commit to ensure building --- jar/postgresql-plugin-5.0.7.jar | Bin 213487 -> 213543 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/jar/postgresql-plugin-5.0.7.jar b/jar/postgresql-plugin-5.0.7.jar index e0fc1a16bf9e37cfba2f063b24b354ccba1d5783..c9ec73e0f08ea3fffea752597e6407890f9093e0 100644 GIT binary patch delta 13723 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 eoCp+yW=ezU5tki^%O}uzX|O8F0VVtf1N Date: Sat, 10 Feb 2024 01:27:02 +0530 Subject: [PATCH 130/148] fixes tests --- .../java/io/supertokens/storage/postgresql/ConnectionPool.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/io/supertokens/storage/postgresql/ConnectionPool.java b/src/main/java/io/supertokens/storage/postgresql/ConnectionPool.java index d209411f..d0b8c6f0 100644 --- a/src/main/java/io/supertokens/storage/postgresql/ConnectionPool.java +++ b/src/main/java/io/supertokens/storage/postgresql/ConnectionPool.java @@ -81,9 +81,9 @@ private synchronized void initialiseHikariDataSource() throws SQLException { } config.setMaximumPoolSize(userConfig.getConnectionPoolSize()); config.setConnectionTimeout(5000); - config.setIdleTimeout(userConfig.getIdleConnectionTimeout()); if (userConfig.getMinimumIdleConnections() != null) { config.setMinimumIdle(userConfig.getMinimumIdleConnections()); + config.setIdleTimeout(userConfig.getIdleConnectionTimeout()); } config.addDataSourceProperty("cachePrepStmts", "true"); config.addDataSourceProperty("prepStmtCacheSize", "250"); From ae8ce1a5f2afbd4058d06560722c2c2c92eb92c2 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Sat, 10 Feb 2024 01:27:31 +0530 Subject: [PATCH 131/148] adding dev-v5.0.7 tag to this commit to ensure building --- jar/postgresql-plugin-5.0.7.jar | Bin 213543 -> 213545 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/jar/postgresql-plugin-5.0.7.jar b/jar/postgresql-plugin-5.0.7.jar index c9ec73e0f08ea3fffea752597e6407890f9093e0..7b06ce4531bcebe886c9b5d9b1335948014ff1b9 100644 GIT binary patch delta 5927 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 delta 5957 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 From 15be5e8e591a40bcf2dce4566250a95d09cb671f Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Wed, 21 Feb 2024 15:38:47 +0530 Subject: [PATCH 132/148] fix: vulnerability fix (#192) * fix: vulnerability fix * fix: vulnerability fix --- CHANGELOG.md | 4 ++++ build.gradle | 18 +++++++++--------- implementationDependencies.json | 6 +++--- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ebae7eb..0f3aa6c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [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. diff --git a/build.gradle b/build.gradle index f98f1df7..3295a3d7 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'java-library' } -version = "5.0.7" +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.1' // 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.14.2' // 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.14.2' // 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/implementationDependencies.json b/implementationDependencies.json index 6c885fc4..9986f3fb 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.1/postgresql-42.7.1.jar", + "name": "PostgreSQL JDBC Driver 42.7.1", + "src": "https://repo1.maven.org/maven2/org/postgresql/postgresql/42.7.1/postgresql-42.7.1-sources.jar" }, { "jar": "https://repo1.maven.org/maven2/com/zaxxer/HikariCP/3.4.1/HikariCP-3.4.1.jar", From 79f6ef7b3835ca55d758a7a86b01e161ac5c63f7 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Wed, 21 Feb 2024 15:45:32 +0530 Subject: [PATCH 133/148] adding dev-v5.0.8 tag to this commit to ensure building --- ...-5.0.7.jar => postgresql-plugin-5.0.8.jar} | Bin 213545 -> 213545 bytes 1 file changed, 0 insertions(+), 0 deletions(-) rename jar/{postgresql-plugin-5.0.7.jar => postgresql-plugin-5.0.8.jar} (95%) diff --git a/jar/postgresql-plugin-5.0.7.jar b/jar/postgresql-plugin-5.0.8.jar similarity index 95% rename from jar/postgresql-plugin-5.0.7.jar rename to jar/postgresql-plugin-5.0.8.jar index 7b06ce4531bcebe886c9b5d9b1335948014ff1b9..0fec0bfef5aca54e129343006e4e9047f60f2e30 100644 GIT binary patch delta 1721 zcmYL}eNa?Y7{>28FKmP?wt{84upr1c?D8or!Y(DEq2>o9AcJ9rU?`!Hs1U9q1{uk$ zc>l->XA%ZYfq|Kuedvr%8IlD}DpY9BG}HJ;qo&PNHr2h)IZivXdw=sh@7I0rx!d1r z?r$}lR~3?vzoPj0DFe;LUFIkETekEd8wnqj9>LDe*@6eN9BRey~j_fA(d? z!b+bQ7mDANF#%U6?{ip}f4GOv!xyGK!B7f*Yp@!io(p6v4A5rIWL1dP8ZFF%CfdQ- ztj7%39>JEOsJ1(b)eE{E%g!K9J(6-M%9goUu+Zukvy2(gPCU!J0U&odt2cp0HnJO`pd%jU7VW26*-{I% z$Rq5JNRaIlc2u}$&$92xY|PZ%G`;@5zRDA7kB22|K*|5WDF7i8YJ7z@OQn$Kf9Pi@ht{ynoqw;hKmKh@1P!%y(uX zF0FtM;#6qAtl~*x%X8N94@Jhs=lCs#Ip#h&-hEdeb=2f_PdHS4)Vb=0IGUlJ%A^3z zR>~hB*Eo7hP`Ij`H;P@|tmex_IY%w`h@nPa;ant~*~ascz_#;hkx<*h>#$t)`hH#r zGog)F;hbpQ2Y7=Zb33mTRNuj06g2rJe~laOhPJws4+`?>;_F2Jio^U7f?KWfC69rI z`uM0XsQDD%&p^Hdd{?c}SKT|0?0}JZ^g1BjN_gwkS}VDXX!W#}k^ncX5S>6?z-I~64p^2*`vBi0 z(s96!B-#%6HHlgP`v0MraM~_6w%kFxV2wItOkuKw%gNLMt1N}K0>)D40H8RPRKV?2 zssfax$+b+S$r~(5msz6^!I|;!F=fb6?F;1&KVC?CQOA`jxAA4BeB8Dys)V&KOFoO= zvt-i>C)LCH)=6stG1>GI;7~SI0Pbc}DPYAS+5xz@h;{?=b7Vm+hZ>;Rb9Kcyna6Ue s4De5`?6t|IKQF_&=F;aeAlt1UHUoOy)CdUAqY~6<%A*{>r+KvHKe`QWLI3~& delta 1721 zcmY+EZBSHI7{~894{QW1wt{84upr1g?D8rs0!xXgsQH4qAcJ9rU$xE1}wjUW9(`nRUuPRjoi`Vx_FCEZxO zfh^xxhgY(bJ_e|vdqSWT{nltTKs^`4Rv4hon8B(Ltu_VNa@Jr1jcj5!!azqn%q6y;YGX?*(4vm8 zKcYakPuNl6o;}OHBeT&{PxJJK`}!zPsJ$MRqK%(tu_ENb=j=FP+QEG#L#~;MaxPc3 z5O-+TN7?f;QE<_ZtVqnF>nGMC@`8S5haQJ}>@W6?a0~ur=Y?w`ep)=-M`pe=8*%AH zd=R%n`(+hR7E_+RmVY2JEaEUEH^x(ddMb+oHCriv zfL!D1Ek@z0a^56nb+ei;7v&ta+#{MAd6{#OaAq4XL;~B+t3^U>E3e0J)$99tG0env zUWI$2^&H@hg3KMfQcy!De?idX8~j!A5UacRpdg=azFzEKahN|Ms9NQVA4BD#em*MB zx#bk!FRJ*S=DTVQSKT|8?0}KE^co<;N_gwkS}WnTQ$Ms)GOQa`assTO^c0{nlyHUB zp-|cgB{+;q09(Rn3*cH9JqK{wNNhS_qiQH2;WEY(PV1mdg;NcnFhch8MFiDD$(l#6 z11`*?gMc-Wju(OZD7c-jQG7f(Zg{sd|P#3Ygz@L3{t0G1}vKEO9gbR4iF znYIIdO{P|W{(mSsoVLq}EqBl^SfdUZQ=B5vL^ssQeE zIhLt(`GD>WnKk+loS6@gDO0v;Um$1r@dDb5I{8_08ee9~Q;ED+_A5)Ck3%CzWG)R0idrJh|8A he0g5i@?}3cE_vDdT-1c%hyrp0nhPiw@M!^U{SSIb@EHIA From 3c7c4a4e07777f788abfad0339fd90dc2a7d87cc Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Fri, 23 Feb 2024 10:58:06 +0530 Subject: [PATCH 134/148] fix: dependencies (#195) --- build.gradle | 4 ++-- implementationDependencies.json | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 3295a3d7..6efdb3d4 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ dependencies { implementation group: 'org.postgresql', name: 'postgresql', version: '42.7.1' // https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml - compileOnly group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.14.2' + 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.16.1' @@ -54,7 +54,7 @@ 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.2' + 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.16.1' diff --git a/implementationDependencies.json b/implementationDependencies.json index 9986f3fb..f0b780d0 100644 --- a/implementationDependencies.json +++ b/implementationDependencies.json @@ -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" } ] } From 404c90527116a7f1750166381ce9df00f3048745 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Fri, 23 Feb 2024 11:05:04 +0530 Subject: [PATCH 135/148] adding dev-v5.0.8 tag to this commit to ensure building --- jar/postgresql-plugin-5.0.8.jar | Bin 213545 -> 213545 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/jar/postgresql-plugin-5.0.8.jar b/jar/postgresql-plugin-5.0.8.jar index 0fec0bfef5aca54e129343006e4e9047f60f2e30..6c72cc5928c827a365151eb1326fc9a3b7dc6d0b 100644 GIT binary patch delta 1721 zcmY+EZBSHI7{~8953GbNwt{84yb1CSySxgEu#1UksQH4iAcJ9rU?`!Hs1U9q1{uk$ zct2=`GYNxc!obYUUUWvM49S8f6)H4mnrZydsA)5mO?B`896z)(yXQCm|MNWOxz9Ow z`&-TZt!DG8BJ%N96hA+u$6eB8jwWNtd?s37+=~9;#*h9X{aaIKBjx`Iy@^P;nr-*b;@@HRm9ITA- zF`@Wf9TjkG;sJ+s<%j#&dFbMlCm2fMZw*!h)boLCg#p@(8LSG?TBC(ou!(kX7V9y? zwMDXJD5~v_X7z&Z#Idu8Q%@zcVvEuCaBALLPx~s$s{xAA<{h6(Qtu{EpxTo}7OmXI zIxq7A&;-#Vm6=w3E*=ZveOR5%$7N6kPlxD-^Tn{E0P-yuhE?p(o%T|BJmN-28vp1>u^ApAirDk(uwzMqGL! zAHc29ep$tn#gu2Sl(MKdaHBP4e>NxJ)K1XT39K6 zh+O09EkWU`a^5Irb*q{$7v=1=+#{MAeuZ<9aCRFnKmyy&t3^U>3$Md))f@YH5zNFk zUWI$2bsyjjg3Rr_Qc!&de^JoHoBTEL5UV@+fFPePULy9dILxOBs#f`u$5DB(kB^9R zZa&TTiz>cn_^w*RRrk&%8(?@Yy$;B*65cws)=GHo)DNwc4C|(q8~|$wEdo@A5U#K~ z7(yGM1cg#DU~?#K23!xN=K+o|5}WpiQ8kp{a2ewXr*%*!!>I;P5Fz{dB7*9mWX+>D z02k-cLBN_w@&K+y%9y25)COfdN~W)wFSDlRQxmN6XnGlNIa=` zOxpp!CQ}PQ|34HRPTAzdmfL9;tP#76DN2!WC51X*m8H^Fz-THR0IW+R6>ukwssQeE zIhM(E`GD>WnKkkV9GQ=fDO0v;TOeon@dDb5I?gOPjW4t0affA7C9Hkf@+^MOmYY^M zs2Q;6D+_A5)BwenCza!QR0idrJh|5< hr#!FgPT5b6OJ25K7d0X{BA?uVrhLi;e40;N{sV{GO&|aO delta 1721 zcmYL}eNa?Y7{>28FKmP?wt{84upr1c?D8or!Y(DEq2>o9AcJ9rU?`!Hs1U9q1{uk$ zc>l->XA%ZYfq|Kuedvr%8IlD}DpY9BG}HJ;qo&PNHr2h)IZivXdw=sh@7I0rx!d1r z?r$}lR~3?vzoPj0DFe;LUFIkETekEd8wnqj9>LDe*@6eN9BRey~j_fA(d? z!b+bQ7mDANF#%U6?{ip}f4GOv!xyGK!B7f*Yp@!io(p6v4A5rIWL1dP8ZFF%CfdQ- ztj7%39>JEOsJ1(b)eE{E%g!K9J(6-M%9goUu+Zukvy2(gPCU!J0U&odt2cp0HnJO`pd%jU7VW26*-{I% z$Rq5JNRaIlc2u}$&$92xY|PZ%G`;@5zRDA7kB22|K*|5WDF7i8YJ7z@OQn$Kf9Pi@ht{ynoqw;hKmKh@1P!%y(uX zF0FtM;#6qAtl~*x%X8N94@Jhs=lCs#Ip#h&-hEdeb=2f_PdHS4)Vb=0IGUlJ%A^3z zR>~hB*Eo7hP`Ij`H;P@|tmex_IY%w`h@nPa;ant~*~ascz_#;hkx<*h>#$t)`hH#r zGog)F;hbpQ2Y7=Zb33mTRNuj06g2rJe~laOhPJws4+`?>;_F2Jio^U7f?KWfC69rI z`uM0XsQDD%&p^Hdd{?c}SKT|0?0}JZ^g1BjN_gwkS}VDXX!W#}k^ncX5S>6?z-I~64p^2*`vBi0 z(s96!B-#%6HHlgP`v0MraM~_6w%kFxV2wItOkuKw%gNLMt1N}K0>)D40H8RPRKV?2 zssfax$+b+S$r~(5msz6^!I|;!F=fb6?F;1&KVC?CQOA`jxAA4BeB8Dys)V&KOFoO= zvt-i>C)LCH)=6stG1>GI;7~SI0Pbc}DPYAS+5xz@h;{?=b7Vm+hZ>;Rb9Kcyna6Ue s4De5`?6t|IKQF_&=F;aeAlt1UHUoOy)CdUAqY~6<%A*{>r+KvHKe`QWLI3~& From aa783d47eaa4f39828392ab1c1fac33bd7d619d5 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Tue, 27 Feb 2024 13:07:17 +0530 Subject: [PATCH 136/148] fix: version update (#198) --- build.gradle | 2 +- implementationDependencies.json | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 6efdb3d4..713fefbe 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ 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.7.1' + 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.16.1' diff --git a/implementationDependencies.json b/implementationDependencies.json index f0b780d0..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.7.1/postgresql-42.7.1.jar", - "name": "PostgreSQL JDBC Driver 42.7.1", - "src": "https://repo1.maven.org/maven2/org/postgresql/postgresql/42.7.1/postgresql-42.7.1-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", From 969b9451b0099e7b1d500f7d7dd1ce14c5810d7e Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 27 Feb 2024 13:08:15 +0530 Subject: [PATCH 137/148] adding dev-v5.0.8 tag to this commit to ensure building --- jar/postgresql-plugin-5.0.8.jar | Bin 213545 -> 213545 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/jar/postgresql-plugin-5.0.8.jar b/jar/postgresql-plugin-5.0.8.jar index 6c72cc5928c827a365151eb1326fc9a3b7dc6d0b..d1a855bd6fe0aa5d60dd5df094cced1d43cfef0b 100644 GIT binary patch delta 1721 zcmYL}dr(wW9LMiDKiCLaYz517c?m9Wc7X+95m-z_^N|lsUV~wUtf7QPqC&We7-S@~ z;{78loJklo69#5(_RtxfG9(L{RH)FLX{PayMopWkY^r;|=Q!=m?)l93_q@OJyW8Jt z?r$}lS1lvIKt%}%Q2agRU1l2@N9NNuKTmmyqJOyYqkl;Mdg|0*g*IVy_@*8Ka{%EQf7c}G_452 z%=AsG0WGax3E!3NF(JXaONsy69>@23f9p-Yn-h+qfum0lJTJYc3D4H|w+ZFXzU+9I zGRDV*5^!Zyz}1QS9Hz@Z+(YG|3sc@uC`G?DSPf9m1+x_fXw#>&YItjn7G^;a?chw- zV}{ip&6Xmmw%f+)1>KHkXW*xvOku?qqwV3eyf>fnSCm(R6s65KK9#KANu*%4Cz&i- zg`IU^5ZWy#+Zu?pW%(>rX!VO&<}_$0o?*Tqkh_A_n?S=G*^O|}5ifI#@{_G>i3M8B z5%xz6DB=@#R9MfRW#5t6=&8GDs{Fn_$`fjjm!)cB=UJR^dGI+qPMCH`Z}FgehN7Iy zRV~CF+Vv6k{0t;q^dl=0v*`SZHH*05pV^_uVLkR2dq-Fc{$=Nd)kOTXc({+ud}lWN zoJD*9TcQ23il>Mv&sxhr5D^!j<+m96nEm8f_g%f!5tGk7?ojnsXR8|$XqtK|i-NR> zQvLw3#_BCb;_3?CC}wrDhA$WC9JSmlni_tYa}jW68!tov+s`?>z2 z=qr%a#;dU>TK56oAjsU#s|3||@D~J4yun`;53#zF4+!$>;_F5I%ESB-K{YC0{1`G1 z_VE#M&&{X!ev!rhG~ZQgyprnPIb;V6&!N`<8CG%vYORFVPW{kIDMl*whLv0ZYZxsA zRD}^%SRD+b4NyYDsTi<1oHhflh0}8YR|M4p`Xi_Y5E?0cypgmH%48&Y0EJO{KSt3n zqNom#HJ4roT$oD-0c)bk3%D9BeU`*f86Jr&M-;%Ey@cj9E`o$<5>%HQ$y7N9eMHUjP?&>)~Mk(vRqN#p~3mPGA~dnu9kdIk5r_0ymMYgP$J0hE^VA_{0 z@8b7tS+vqc^)P+wqP2jy9C{IOD2FNmcXOx|uwnu209;%^y8(r{GNG1B4N&ZPQaP4K pWl;XfleISG%lo>PFZ;=H%Vq0zQzM+C3aA7d-c&%jfKLl(%YVwY^K$?I delta 1721 zcmY+EZBSHI7{~8953GbNwt{84yb1CSySxgEu#1UksQH4iAcJ9rU?`!Hs1U9q1{uk$ zct2=`GYNxc!obYUUUWvM49S8f6)H4mnrZydsA)5mO?B`896z)(yXQCm|MNWOxz9Ow z`&-TZt!DG8BJ%N96hA+u$6eB8jwWNtd?s37+=~9;#*h9X{aaIKBjx`Iy@^P;nr-*b;@@HRm9ITA- zF`@Wf9TjkG;sJ+s<%j#&dFbMlCm2fMZw*!h)boLCg#p@(8LSG?TBC(ou!(kX7V9y? zwMDXJD5~v_X7z&Z#Idu8Q%@zcVvEuCaBALLPx~s$s{xAA<{h6(Qtu{EpxTo}7OmXI zIxq7A&;-#Vm6=w3E*=ZveOR5%$7N6kPlxD-^Tn{E0P-yuhE?p(o%T|BJmN-28vp1>u^ApAirDk(uwzMqGL! zAHc29ep$tn#gu2Sl(MKdaHBP4e>NxJ)K1XT39K6 zh+O09EkWU`a^5Irb*q{$7v=1=+#{MAeuZ<9aCRFnKmyy&t3^U>3$Md))f@YH5zNFk zUWI$2bsyjjg3Rr_Qc!&de^JoHoBTEL5UV@+fFPePULy9dILxOBs#f`u$5DB(kB^9R zZa&TTiz>cn_^w*RRrk&%8(?@Yy$;B*65cws)=GHo)DNwc4C|(q8~|$wEdo@A5U#K~ z7(yGM1cg#DU~?#K23!xN=K+o|5}WpiQ8kp{a2ewXr*%*!!>I;P5Fz{dB7*9mWX+>D z02k-cLBN_w@&K+y%9y25)COfdN~W)wFSDlRQxmN6XnGlNIa=` zOxpp!CQ}PQ|34HRPTAzdmfL9;tP#76DN2!WC51X*m8H^Fz-THR0IW+R6>ukwssQeE zIhM(E`GD>WnKkkV9GQ=fDO0v;TOeon@dDb5I?gOPjW4t0affA7C9Hkf@+^MOmYY^M zs2Q;6D+_A5)BwenCza!QR0idrJh|5< hr#!FgPT5b6OJ25K7d0X{BA?uVrhLi;e40;N{sV{GO&|aO From 15a43513ded2796308e6d5ca0570af3aff601d72 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Tue, 27 Feb 2024 13:44:12 +0530 Subject: [PATCH 138/148] 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 zQ{61^+b&EM3S&`*au#{VuVgl0yaw>LB_59It*k*fEF87y%T+oCbjgUbiKzfpvz;@AJpV%Pq%x z10{NZ%8sCT*=i7^n;rY)<0CV#XZ@qx^w3YSW#tI^9okLI`8oiTQQuN%E;BMfmhwY_ zr^T8QbcqK)-G|hjTw+h}-zM;bFcr3U@t6Lj_qgR@@cW~oUym{A5cGt24L@9V9&y_I zydry$Lu{};D%21g>1aNBU^H8x%|-ixpsivJ+VyZYd5-r>6=k zp82%HL_|X5Lei{WG7om@39^_VRPsg6R_%YK?eSlpDN2hyz={H;E$e+Ut;^J{q}8=(z5F#o*efC-0ArBDvvb2ukMG;WXTYF$3_-UTNQWq)pW?WC#r=^ zpW5&e_vU>1+|w56I>FE090WfPcH8FKSR2iM!h+(DbU5^c1_`S#7Q3%5)2DD`rS-M; zPv(2d_U-Uy{Y=Pj-{YqzBJOR8;4w7=jj=W_rZTp=dh0E7C_viMOhU7|TPMe#|GC|S zYmli^Df0NEf9_)yILN^m&z-I892Gu6p+74V=B?O)e~Y#LEscrgKAzE-;Pe|><|ysp zMY_7y(9Wc#i6uM6!yao0%!mRi*jagq-mEh?IfaOR)V>KuAOb~8ubER`IT1~Q=)H=0 z_rG;c5)wt!!MG(aG76>$0XJ!PB+HB5AG@Rxd8(&b4_)Tl40 zeCGmuFQROj&Okq%*2Ya;4iiM~WpOdeqaMXJ8>2e?cu>zSl^h;Va`K?@q7$ohJ@xooOzKG& z=Aae%8vbkEj?gTJR}%-0Cbh*^6d{4zt~j{^c-QOzpQwUV7D<7ui8@84 z#YLuj%asK;72k|m;theS1q?~h(_Z0$Ri$iXN8<6Ss2qD!y$cXvNrrd0%Zn6MHCMKB z!R#yVJ08P3x^1e@-xzQ3{upVSw6DlJ!Kia9k#5jy-OHiY#hv8^`YBUEnFjYF4i$_W z2ft+0Gd)it20f=2d8bE`n4)U! zoW;a$HqTvW_$qcqAT4NMpbnXO8`p$@v@!%pa$J z$otf>wGdPh51c+~=`A0{yd+L2V%^y}-Y;SJxf(h<(lZDprEh%EfO)KtCPu55i^Q?8-9_ zp$2m6DHT(K0+5W{jnhA=dctmuuQ_qJ5H!NFb|e@B*5rC2L_b0k;!qr@cy|*~z}6k~ zDq(d$DMCR2CGTealHhERc9q3OO>!A6aiXWDvBSrem>#2@d2%V3tLBFCMM#nrq-8Nd z3Ll3WwZ0%x8NGlqt+LyPKp1DnH)Rw@>CL#7O0vUD03_p3V@u@QcW25JtIAVzPrDZ} z7yNRU9N;16=QSiebVV&&oqXk*4lWwq+yctk<9UdKKDL`7sAtMqaVcehPe1uoG|zrb z+^JnCNs7&#Uu+seHZ$c)*YD1BE>4tUqod8W?Cby|R@SjFXD2R6YW4m^5Tvgx-mwj7 zzV<9{_4QIg~J#;ik-DYh#pZyeRM&} zmiAx{`la3w?{KvEf)k)Tl+3)-csyy$ zl*g?zqdOeT@Gjry+GKM4k!NAk8NArWwZ3Ur)X;|f$6h+Jb~-M*EK%yN5Pt$`^Y^up+uo;r`;fO_Xux#m|#a6 z@Y7jei)iL`+!OhOrbi!(;3cpK*;rczv=WMd?m%cGXd8)<1U*3h#^ZKZrs>HP= z*suk#9hhn8tZLYoRA+4Am)`Y0YCLhBwDooK9K~21{R@2A39AuJ+HTU$RA$OeG|?|`EQ3aBvoA>_Os;T zAQB>c!e#^qC54>qjL`2Ms6t&#af;$fxgIU65y*@2kP78EuRRMT@OPnc;*8WdaKc?B zX_U_*(~g4JP!yD@(yh%)cl9MI0_a*=;9FT!%MeY=P|WCf)Trhym2Ry-pMD%UPvCYH z&!TOmF4l{3dn3mU4f=PClkgvsjHq0u0g4m${F((G>fFy4w{kOZz5luIKBxci&=x=vijV5{>Rt@B%+^Eg0H7F7q{+GV3z)=`RZuvT+*~gbVrVs=nwtI^ zirM28*B0!k8oU|yDd4$MkuB)AAXnbv*v-RC8MJ5=Q~|+_MR1fZmPpMpD=FRJjBb3M z@|LTLiJY4ISTIfLZ1PRfH{*cjQ?+sqh*%|N7&6ars%Dn3@;+flHIa<2&|UL5Ciwd$ z|23Nqw8bJThyo=h>d~Iq4TiCNi>D~kq-3!9%glxZd6#H55i@+2+KzuQH>Zg=pjg5) zh9K%1uFpA#=R>>^kRTVu1cO3VCS?JB!YrTz4g1X=JfjnNRBj|YA(4f86&HN*Z2O@v zf)Er|*Cl(?*_+DDI@K9Tzi8iE4&%&)J4__vR1k6}ZKg)n?t9)Uh1|uwO<5DNeg8bU z_QrlD#Qw&9B1ClgLhz;3L)Q6bT#)AzSN@a6|IPjX$F_7whv}UDq)vwZnbiyYrBA-X zP#>G$iy?hr{{F>5^Vdqqz<=p{2VZQLC$0X#^X%jg)sS2u)wx@6t3zhsP!dWn!6#b; zQU=62=}!#bW5qCpKSYVL#eN@=O-Y65X{&bI!0z|!fx&Ku_&nfebE15r{wG~;S=+9$ zF&*cY@1TpzVST(~_g5^$Bj}BS{d(Q+KJzup<-GO2>!~k60q_&(X2J{qsqB;0r-0s^ z1>SoJkJ`yIkb#bFt9NEG)3|Wz`K?bMJAP=s1hY!dz^0}{4)^TpEg^Z_Pyw{2a30_0xWr^PZ?@^ z0KB+4ORXeq!WbID4=ZR12NbqX3Ez(|+clo~Z;=20k1nNo zLp7dI7+a>nP#C5{z6Qf#1cSmjRGNIvYrXz5f7laT(e~ou-@f`laAl(;m*GGO_H!2C@bM8$m zuM0F5uMGs&`aH#JLjiyBLj0r4*EF4g&0#M#_TWzkgc_TDVQ-jtQrA+f^QNpGN$g$Y z^#rGlUPd^M)u0&0G3dV56J8qf1gio8A2Dl2@(_~GjCAlL0^WdM z@1O1;tHC6g%wU{5gzpX4G`Pb$x#k4}jn(c(j|onwBEWC?ymcfC3>I__If(*OV5$n! zG?)%E5ESQL1I^*4=5TR&VC`BCN|rZN;tP7*^(|$n`Chlr8zS`>lLGm;CLOQAEGR)u zayK=3{PmuHV0oZ!eXPzHP!r5^G$;j>?4D$9=Aqu0@KmKhngq5bUKzd21@js7MkrtFHdKgHfR+6GrK)nVYScA9S z_4UzzcMTcUe#g3!HE7@^a(6@InhZRs&-EJkppik3dQaG0*AVSC+YW`5=@1}&ZAwa2 zV~tKa1RTfX6vMx}n(<^m;F^LdqQRr^7(#_zS9^jRJ;AxbU?6C|rC^jkvc`9m*cMTE0$Npn*sZ}H zLSaO5kLiOB7dEsiE_jlGaLl-L*bhHb;VBIckX`Q;k0{cDi&kj4H{77YGYm#q#mU0S zB>|t0#uE$H`{6kao+lW)g#+b*O`hN!j5=KKBKJ`IhW*ZKZmLI!sQJqpyaKNx@9}OJ zjWt@q{#Hvgr@GQQ{+ARXR=_f`0f1|PtOI0Qz) zTI>!tVA#?W*u+2{Ra!c_RGGD-$$aP|4gNq+h>c!9{r*IQPvMUkK_PU1^?^nneU{RA z_a_bh44-2kA)m+7L__&P3QNCWFev&mu2{k5rm&~pLh8T5-&FWYgRkN548}SSI#qg{ z$7Mm6j50qm8tKa5q*P*w)Lw-u`wje4g@0-AZ}<;`>8U-QU$vxi&fMzi+UlB;8XO*a zl+G3f$uf= zfe4XpVp36AQ@dntb!GXoxhNFd;HT)8Fs3nqiF()<^^+LPN;&L{^|YpSV=_}zrfST^ z(jw+Cwhh`}e5>8i#3i>ZopocH%DQW;2kVLY7%QqgkWe?eea)VKN_0kZMwMBusnKFu z8tct6RFN09NNMnP^Pw!5CdUH51C(zjF4XbR3 z^8jpd6D%)@@+O^s<*{Mtv$6a*7u*b)yRi{$q{>EVY&0uiFt=0O8+4z^m*(F~{Pj9Q z43?(iJ=x-8>*t_+jnP;UaRA-&@G_DmqHD3n#!+HVuJZ*#{K^E4O{6QXpfMN`eU2sF znG`>c8=ccr$7yT|J*CufJkr@THeF>iG&YkR@9a>=WNk2iAd04JaPXoqFI3gM+UKo{ zPw0^HCYaCEQbI0Pg08x|j_gh?@1Zv2UF*kTyLP>&1tSqQM`NXIuCBq0JT07bwJ{#= z&r(f5#2Ju|VPF^cI&>I8qs+f1FpY4WudxNJj6pwr^tVUzs;-9SvUIkHm8)#A#wu84 zN`YcEvKwK4G6TioBxPI(YUtT&T?CL0p;#K+cO+>LqY2YSpOg|?)7S}YiOQ-qR>PJ$ zr;n}AQeJ?^&GjYW8gC zt^B|!c8bPo*{OQcE|R-m)4AT8ktVOwui%)VzXz><@@iJ6vU-hq$ia`dlF9dk&TX6W z%X1$thHVsgQ&X+C9%EkS)z~_=9*wIvG%pbJU~6oOeTC55$Kip;JPayFB^yp9l~-M< zGIZ3dQ{kZ{?$r%KWf0j~5eUxSJM*ys4dLkU&vYC>+tV>_Qi2)_F&vAB20e{| zjUIb{y3Y2jLCP^`NI|AMAikrQx!6YZ41=CE*kyw;R2f6mW{tJ5ExIFRZ@ZuBVzxUa zQ-%Q?`w4@Q(O#DN@!_UG5KUAicEY_yQyN_C3MIv){8;Msm4u56mYV172J@UAJgQPhZXzW6sFs9_D;fE0meyXvHDOuT5 zA3;leK0WD3&s<8GV1>RFr^AhKtIDoqFoR}k^!bHieSQI15r|~XDQ%k1XM&6=mMyV3 zQb2Px`EE5Nu-9trI(9vSUb@+?@N6>1HwGUZCh<&$$H`o2XRc~tyEiFMjP~{V{=;6e$jKS$g3XGF%$iyLgSYx|?*dvB3>ho2Qwl{f$o_Yhu1#W*m8WbF_b?LhN zDDu??2}0p9Lg8@+mmDp@Zv(DYjqN7m+}AYDv7`-=1>dW&C)qwdkrx?$7~Fdlshn~c z_cM(>#SZARnNbu{iCK>BFi!^k8I3(l2ECVs4cwX&BcIpU3+zRGa=}#Hhr2L;i$Xe( z#g?IoOXSNMdxgD+^LyrfmEiZ`+9*imo3U&X8F^DBe@AG-qx_x|R!*t>^0N9^2_Gk7v12Nd_r__EN z47~ObZF`V?L7wZEG?8o9g1Gnr`L=Y2t3mK}AsR>y^HiOy8r+X86j3$1)A)-%N{cI1S zL*kFDNN9c(W4V>8&?m7NH@?qsAly`3Q(04Gk}6l|rwaWwVSq4yFsYL42&t$OlR9J| zOFbLCb@bzK`H@NJV6xcju%g@zW-!s=p>>+^DRAC5%@eU7`)KQfn zNsXz}!Z0CU6^3iV2w`MWGx8B?TlzAIPUS7-?oe32H@iibPX-qqE&5w7l?U)b1~6f? zCKL#TI2<*)*L%=^4M07i@)n*$MlU4PR^_WreD*0j%O#BEbGw#6*CmX@Aizj>^5p9R zcc?)XCZLbmw#i8s#)FtH94kyxg~^(5oG>M}Ag3i1_B2w#i&6K8L;PZ|&*u&CdH97g zFF5HrdluQy85+Azn91PL4(5uELT1b*%wpi8R40ut86XIMvo$tan8V=Nt{f~SR~JaU znXADy!aTb=n?vl>zBo`=pb2HdLX=JPsECv`0X_NRIRY|4{@Hf;sVXdHu&CpMDAyqh zQCvbLx_hBOqlaJ8WgbTMatYN8GD&F0PnbB%v)SF)Xg{A0z3d<%#xjfb zsc;IBxi+qNI9jbqnvFi3L`-rCb%u_YqGN}KPw;5M8ey$otzH!j5VS1}Mkd9v(aSPG z6ue|5)-m{9*GxlnmIYA-Ut)!ygHz!WP&CsyxsBg{O|*c^Qy28$sIy)O66bIjnVuB6 z6R(t&(Ts-BENoPTO`5P-Xi0P=Qlc$7|KUJfG0lt=%VlC+19B=)(}bT0r(>wCPc`YZ zU~t2c@l8_n14*bQVK%vEXKBLO!Z{4G^o$Kz@u-h=6y5tWmvA1cd-N@}6sE0qhdi}@ zjDl5v;R0G0WTO&QL2n~pu9AUrcM*AGKc)D%%+AFU&Sz(lfw)u?E)y&Z(@JS2iE7+NI!yYlZ7n;d)KD zLAa5@%oO9cDt9QfDG;nT9b|sg*K0X z=n|e_pi%~>)?4oqcGI$xhCs8gKF{w#wDM5>Jb4YCpoes3uO>Vx?87E{QZ_Ladqr1D z#p3XlCj3lzN)--h!qZeIHYX(#MkHi^iHFL8BMum9do!ma_5G|SJjV;rc!zZ51xoT7wRFu3I?S^i|5W|Y>~G~soURw{)} zL?L9z?3)Z$by#K-ySK>i8z8E}ubo${wfUN*{`LOACVzrryhSnlwZgla@SgC0J_ZN2 zc@>@;#PWT^#5f+gv z3t2Gr#3|D4U0ad#(oErdP56P_mfT1T5W_a^iVM9(&_qU+2mPW>ZhReo1vL{jQKD+b z9|Pwxv|m$x9bbB2THe z*jp1b#7qVQ+k8OxmR|0{+5S|Gftam{Ibt6MJ%gT&c!Nr;dCy0+6tWGS7IQVRpV*%Y zy!;%;7LMG}D~)Um&VibLI7l4KU}TbmQWvWz@BA@G9HNOs#bLD3P^<~=QoLJ9kPg?x z5hP>7^>HI^$fya--HfmEyM21Rh0cvQN)tzu&ypA8v)DMF>0+TcMiq-RajaO(U|I_G z0}b9F_81JenEAT?(Qz%old_v$?NWo8t>Pec#%sb2;snNebR~6vNFm=yGa>%Pe0@xQ zlxLlhvlNff#A9hVqeW@at`ifM;$)g{qz1K3)S}kNi>cxi1~pE(etM2SW;fLq``T!# zK|GGIZ{%zYFHg`2kVPVBj2h$?qFR*jeBh(V$0f5fdFDH)@d^>~8wz z*2L9foslcnr&J<*GdwsdC2Q@fSyX4i^JwB4aV^p$4#9eSg$2WlyFjs=xijclqE{2w zi8LGMLmjbyfE}702@IY*s>``L%$7z?^b>*u6A`Rx2>3nv!ar)+q=_5Epza-58qRKs zj>CHJ`tz>jS#?x$#jx0{iW@a?lejr0e|q8a8e`SD=}%MCAHAHoNkuAgN@s6Up?An< zYRgd)IO1D0@ig%#7_$ndDC9Byh&?CeL=>Hi-JS zo06k9S>4F9Ky-=cF%VXbO^5yB1)6xFcoA)QhV8@lR`%ypq9j$<>kBz3FP>O%pRf6|cwumoPeAyk5LP6>rqUt>QKY zGm=+-h)A^R^Mqn+ruCwqSb2J*i=}`a%}A34yBJiBk|v_tc1^sQcIzoZKOx9VbM%|@ za3G;QTf&~krZ5jEV*CVr@>Wf}O}t$nJm&=JJ)z1qc2s-4t5MByfNGO$ge521otk(T z5zSZwoQO6xxnE@d=`zQRG6`%5yz4E=4A6qdJt zS%#QrKvcyS7z|BVz*rJo+e`*0y4i~O5`$5*1L1}|o@vc<`|IzkHkMPNY_PA7q}z0%|i!8$0dHkCqen><*he}_DAPs zmTeHtD9N%4NxJwcG3U<=Mz^om{(wcXuNVKKiC<8CuR&NcW+Y*M)x^KqNtmU7B2&Cx z*h@Y9T@(KyexvWj7O5=X*M+ArlX2GmrHTI*|HGhnbSb47^Y8DYRy>OOw(FpKM!)I$i32CTXhFT@y5^M@oUGbsfDKcwT$d@{H}QlOh~@X;N<~ zL-#MNkawB=Ob-F4EKSOmauBCbdpOp$!XO=R8eXu6w(Xy+IMA*}%q0%r_%GZQ4X*h$asVO4gHN(_@!md>S#MwGh zlSUC+dmC)!0PFMKQUL>rHV09qF%0HA02JLxB*AN=KBEqGVNU#*=~gDF{u-}H${`C(xz}^j;lPu zMz6lLkfp_RO`1V1`bBnsL!pDw7PvQhqRpw<@tQP?rX16CS&BS8Ta)HUrTWY-fBHzR z=Cj>x4gs2{N%INL?ld*3vxp`}@lKg0EtD1^Da>+K>*g;6DS|hDxFfzJoBf*??Wy#Cs7iiK z3P?>Ut=84&-i?8O!yKD4qv-(iCWD*1Of6*_McGnNlR}X-qA|c5gLpHdW&z7AUXwO4 zI5CM4*jwMI_m?=XSJR_bh=uNsIIo4P@zbI*Vx~ z?I$Chi2=OFs3(n}N|JK6CY?ir-6%_4ex!HG=O&;+51prfN$1l;!`pZ$u5cBfxKNWW zl77ly1Tur(?r#4jhi@az4p+Fu@+AiKX@uz|nsg~)Iy7GF>WE$lH(bcS(E zCU>0<;Sh{NaL|k+FmzLUhbG+_F}2p)#sq|PxART~Z5y9fl)X5)S} zQX@J$!+Wf0X|hI>yC~e-E za4Nljph+)EFU3QRW~t0i=g^z1=np-I1#Uem)6 zTCSzDDz)=<97l+@jY~S5CB30ZZ%V(?s}1#XwsMD zlBSS`G)wEFg_%YTP~--2BkBgwoBpOrUrApx$T8|3qSg7v2Jn}4nYd`-LfzQ@LzBLL ziOk=|!y6fd{^h(wVRT7?bwdzNn8fp(q<_-j^SSh2P5M^)PM=>y0}^?wrOta#m-Sq2 z*I-z-kw^3nYSQ=854!JYxyM~q<0qoHyQ_vW&KSy|$&A!$q>W}6ZDX*dXGBewD0Nh< zJLECX=t~@BZauJ59*SCDyJQyq_(x;mF(h@Dr=hDoq(QT2XthbvJJTL z@t&I8OYW^t0->POys#DTr;Unp(c-^RnFNltDMcV ze$Y&e)#PGx`DY;9L>1?fLF3Q2kx`S!@%Ty7?GU2^?@d6_CtV^EqJg<@*WEVP+Qn_A?`Gd1xpaSwf)rO73f zY0uFQ+(?*v!X|Sxxs>(;Nsl(6%kwmOzPx}zFT)J;SJ812T|DDlM!+xBz05+pW9e^YUyveu81dT@+$dc2Kl-(V&YNi z3Ds2w>*FKm=ek;4nY>_{93-@BHThK8Z65Pu3>zGCyJJ%hTS*@mraDcompyuIAx5j- z`gvZDk21-YR+AfKFP(to36}@fmScQO=jBX0q_i`2O0nN*C=PIz z?9=2%*{=tH=DDV2{+b3)sj*eD-Ko3nWpwP8$jVKcyg^3aW=xx}(aSJccUbLh=+J+Hi3lUw9}EewXWv0ynC`(Jb^ z>^r`kS^kM8pHAqHZg)9z+%kOmOieyZKAS;a;#2X9n_uZN-biPV>s(DfPd*=Y*9q}h zu}Ni_&(SFR=mg4KsL2=6R-gUpoE!^f;zj3TO}>Qca{AJGn36iaK}0CN(Pf%^IW_9n z>~F8xm707N)p+!OkK3CgzTGvNe64&P8uq#dPu==5I_ocF^hp)dyiVf?ZI7`sT^J_c zpvgDV_88-B_YEoWQ_Q?L5TtRyU+MQ)b}%ArwrTQBw9&`Nm@RcE<^TYi$k zgB?0+(BaX4Q1q-93vsMMkZLhLLcU*Pa7uQBLjxe+FpzBYuTO`}_t-*7&EARuytsD)sW(?reFeN_2Zsh(qD z=`QAM3TvmoX0SY&PGfeKjPE$+3`H~lwCpWs6&KT)vUZ1Nqr{;*+FS(tiCEF- zxrdf+e#2m~LpL#tuPks`*I`+^{Lr~e5+U<6Q%jfDr6SfYf0xRNiMamsV^}SnedJ8J zc5J(U+0Dn!5*XV}99+>JEwFv+d=hMo4YF2tnVo#*%z&6qEP%!IXMyX_&J2jA_AUKI z=LjvG{Uxz@&362<>HwW`YU$!jXJxXu>6VV79(%MqG;bwURkdZMwTtGisI4ejOnWp& z4z)C@1_qYQnKQS#y0)fr(cFsK>azJ2Wfk*(E$wrb%$>JnZuJ7ALG*^CudeKg&b1Lh z@+1d?jPgL8+qcXe^wMwRqJ$5~xGfrhH4cI=(wla~l%tT1VY6q1gqV3>x=8? z%yMrXZKk`HGpn1?w#prDK-HmfC}{rdj(S?R9!&HJWchlZovX71@MEjb8 z2ie{^hYZR)r>E&PHC$-cPOq72QFw!7iOJuNyk#{0Wsq%oP4xUrz6imPH{Eha& zC1rD~k&|U~D3x9Ww)+UssKb4g#UNwQytmK0x-1S$v5t_>wvDnW!{k}xMuoHM83}2# zF)p3G9B6FxhGXLW(p0EpA2X4E-u8)t8OQCC4);&T4sP(xA!rNefNsxPcU?6)95P=|`xKUfBuDA8;hE>0lo=lgiX> z4oAep3&h&Zh+{ls07;esgOzog!d31TU%-tNd#R&<9)>LWp^4xuV0Gt?^Y&wOcI-fCZhs>O_3eAe_b_w+3{-u$ zOS~~Y<&skj(|L}DN`O`2YE`LakngVN>$z(JG02B#8D)K?KeAZCF?C|eh#Aa{57w;_ z=wiq2QaPv$orlza`?J~Y$^I2qxR<6R5Eh!~N>WL6dI<}>$P?F@tJE8NL5@qEwIdq? z_1-ltdR$NwD5y#@P?%S-~oOc-SdW<}0 zh$iDLWenABceSxPCwr8=-<7zlLWhU)qy~zPRV|?&dTKm>jZF*$Z@syJPs~JV;_BJN zQ=yLTz{IB@a%{F2sR>~CckbZ~o0B>PnuFJ5dym1~PI-mh-js*8#3)Iy63;k0%;h;r z!u3v9tNG|G#JXHD%QCw?;f``?kS&zm{GLq_vf_|Mbu$dU>3YuMAq!9P{>DB3C$L^- zo~6+40or$eb$RLI16YPWM@41B=0pP}=)HL2@9EbQ*Bl@A zj7W+idH92sWp*c0b-okV>B`#M(pMy&>EUtQ^sB2WfjvjGuw@G+s=0_czm+bz&}~nSoD~-~W$}rllGhZzI*x^o;JOz}KuI z7+Jjx)AFP+i%F-flr z_?jF2`i~yw4RWqk~cVOP^f@J9^RWzUFNGY1JXIAN+YTu1zPK?c_x1sBj6s-xkUI>}&1 zYtK4-(jJ2=yO0DKASreHT-KvrNNj^yyQ{EVjb?R7mn(@X>-<=pqTWzKIWyHPmllPXv$BG4-~8ueNP$ z1CC8Q``2jJ!(W>rXG547^WJlWstpz2WNCFF^%jm(0h*>$E87U zX`s>V_1nBHvo5h25vwETU3AUF2vSCN*Voe`3s=yXbK~=s#-iR(%s#)?O^+B1G*4o* z+c}<#^x+*#7V@(Dv`}<6{%(|i44K2h!e3V$X;bL1=YB`i*{O#ZjsW!wdN$&tcDwz@ z?03J~vfus24*IgG!_2pPXK>-sr=9^xj=ku2kT!=34{3p`*{m5l?$Ghd*2N)iQ+5Xa z!--aW?KoeI>InuHhj2*I8r|W#2CDOJCYzI9Z`m~5IRrSe(WSk+9Z5fb8W?mB`0A}Y zpLh485Ltewm<#Frjlh#xG<1Zc_CvrS@rcR8i$ltzwarH5DEA`DJq}~d#ljQ( zy{lyADGxXP9buydk=Y&24VKZlT)IV@?O78Ddi1@REE{*1`6C|e@c0e9+i=%2M#VPc z%$qqX{#ggety||%2eMm#-!^+tViiSfnEqN!M8 zz0>me{BaoJPOtNM{oe3Qi~_6qR+#g=v{7F=-(YhrL4%%ig(nnV?4b%r3|o7l)P+Og zpu0&on4zAEIz3%~ouSTD)#EjFmRiDKXcP^vpMu}wat;$MaYsrZ4O+0+#XLwmM;_Wu{gv%MJ38_195>NA?3vaJ6r@UNI&@R#YU;84 z*Zg#Kfm)`j3pJrYUBnI8Kn{-XWV)#aL6 ztd4W3D^T%6Vdhd#LL0|wS?zjHi%UHj1($xT;lFAbD6F;#zvI(xdU~~{j!}zxsb~(D zb;3S-wiZ`^E5imsj=m?Ps%xDq9c{NLWRTnDCWWYL@%t;F4^p+Hl5fUaT~kssS5>jq z5Hd+y+?zLhg2fHq_3ogzn3nbzFW}#4X8;EM>7xua-HQ?07XA)%Zvs^fFgP}OpU~nh zp{0l}bWv3dvxg@ot*RQvh^+ml$X2~E41sPSKri`!b@@&CE%|NvUHN_a1Npb|@8v(p zpU8id|0I7d5oQ=>^gg(5pav0VXXStJekJ_dh<{rU;I(s!91Dg5GvmWO5Y;s3$|HV^lSfA|vHhrgA-Gag=z_!6`| z3JRZqJ_8xSU$UpLaT?(B=An3Fx;hTMCgC?177?8ps& z6Kx+JAOG+U+lMD9s5$)MnFu#Qt`rpRfn%aP(y`x(JbIA#kfXOUu~!t0u#e(}y_O1V z4Uj&=YjmdyK{{?LNRK=UGgnkRi!|N?voq%Hfdx-P#Z(DpzG?*u|B@$QX|9y9q7_bh z2{Ljeu@!1_B^14-tx#9=+zv>eDi=L}mn$E5glp_vSb%*mfO7t+b89Tx0)R)5TaTjl zJ%O!SQAeJKLGS`{?nO8TUV^FcGR%Qjp%Q*>a&!<3SGp@bIB%;=?5dQWhU8T#z3`i$ z^v3VKKyn>~sVWGiD(v`2^Zka@AB%8jZG~}!a{04JfVB~X7mwj6z6j}m{L8xK zg@~G~qVTyQj#zgXgS;euPT-$9VnY!ffY%YRH?YN<&>P-DfPM|b;B7?UT}14ClR^EU z7e|Izb*zpI2fQbU2cf`(8~Md9BrWA|KZM#O;^%k-)la-C=#Q31Mp#Ot_#DQ;7aZ<9eECR7Q!?Og4)=JI3FDPaRCUHNNJG;1UZlc#X%UHo3-I4H!aPHd zE3q4TJguPcQMjZPuG(c%P*;SnEn=Kzz9UV^QnK+KQH7-|Ifh^>*m^sEY%^5054{ks z)n6#Qh8Sw7OLKZnm?=(@&XX8QcPm`CYpR?pPgQc|XJFe_=$}rL_GxoN^ zzOA52cu$;^M$Mnz29tAt)mC^mjQ zcKTMBkuCL_a0g7vf;Y3^tt@yuTe@%$yqg8T-2=a`+XEl3ue)Loe6|DRsohWzzI+t^ zL74q_$B$`dA34;=ZO~J{e~{ld$dE=zZ#!U+fdATlU>|H-kt^?ih93_gKhsc1d%=yy z=QiVWJC2xN!dECZ5@+S*C_(`LMqd91ne#2S`X9DBh$G7P$l4!Jn}38+$i;Dt!Bi$f z36o$RlVKs#U?uAgC$rvA&oZEe^?`F)U$~Ox!p*E7+|LHWel{3>$%ep}Y#4mShQi-j zK77wcqHQf;y;&iD8_0^-a5fgd6|;$K9Gk)>aFJ?4q0eDOaF@~-VJcyQLZg-nC&On- zKczqS1zuGKpeU!oez;Jfkt`iD*my4TGKo+`9N8RQ9Qn6FI6?{_vRTStg_J~LGn66t zO^OODZ=YkR$PCE(9viVT)ROO^5B^1E?|l$@s<6N3EEOJqItcyn?<{r@W-yUHy0Hco z{=#~sWpwXuBkx1;*A1q;FG41fC3=rlFtprz7*rJPWfFruEPblf%QZ=Ul=T|Z%5uiu z&o%3KKF|Om$eC7v|Hk3JN%(I%HQxb!Rr;GD(_c-Zzlu0XF3OhovRna*#Jy~w0EPQl z-io5#Z0KHpHe7(IO3{muS0wRUxzdZ!qbOHuWrg|^dyzemtDp*Et2F$HVK0svQ&s1O z)B{Lj8P0?|(VEfcqX_4n@Hqd}h3{gt#lWVZQ#Tc4HXYL049I3PA(tHwrK|)Nu-UMP zl|luZ3n#GoP{S6$a<&i!ZxIUKVmOsmK?6Gh0&EF?oXJ+80Ir0K*-3B(TLs(LDR3KJ zzmrwN-K+-gV@u&-wj3VCc6+eh0c`g?wtF4hz0FSLg0}#LEEg_ThM`c%C`#QU0tN4x z_U*lhK;buCpaghF$ybJRY~NBwC?l-`#oI>&Y6W!v0ZLUStE?MSWLXvt!W0}taNHL_ zRxzM|Vk1_!op4H|%4IMVksMdHfs-?y=$t(4W7ge9)A&J#{nKhz{9AP%?NlHWosk!VVgMM z$6&{OAe{qFWEo-tK13;2#&O;a;V1~$b1p|gX;NvWK_&V&MVX3BN`n(Py(CVrd=P&`&}0j}PKc1~ zAWX!+-$9x=YHedaq+UpwW-$F+LyB_>3iI}`4K{L1&!w4|=kS4eCyt*FBlZuX>^_A0 zx63pd-7GxMF|o-pvB^=Uqs7*LjRu;?vUqt^W;k1k<|M7e6DXCt5yCwvmAetjy{KJJ z9uX@sGa{Yv3QuDIjEs>^9gU(rY!eY&ju*8)h(N5NcovEMJalI-ppkeU(Rdm9u@_+g zdkIFcS2#*D@ihYsl!$60O_WBOD2+5lX{0GigDj#nfJ{a|dr|5ZD?-YD@${Xe_*WyM zSn?ug$np<7|Py8 zioAzP_&yY~-@runN0`KaK7h&WLv%g<0E_W=75fC;j?c_6XEF*UQH?0H#1af<<3~ML zqV8OYT(ebte~c9xX9l$sqqzlkRa|y}p;yknu*)y7>x7wx89NT|QHE_P*aGciY3RAq%GEY8DIDJy$ahj#ZbYmK5Hw)_ zgiM4lhy5D~_#en)|3%_`i$THv&^mkMJkEx^A$jo(CmcNTt=__q>tl4SmEHrfS+f188X$f7T0HFzzJUn@oE zyG3I3W$l=@+m*S0hFqR)ct%;2O<}htctkdaO;AA*(jiT7LAsEJ#OY=lwsea(lw~5C zW$IQIM=!>(#kfBR*gONUW&^O^9I!igM!_!I$U?wG00~TU( zFB6ttIxGQ(0h3j{BX-F$`aykK`DhJUNsy;p%qCu}fKK3(P~i9Dyl9V8$Xa z#ht_(f$b6mTVNh_24)fhGZ}$7&Jmc1*g6TyZqv-KH_Utn8lWdonaWL5tt*pcfks!R z<3SQ;BSf=*5TX)%(;U;}MxdhSGAt$+6=!~e;ykG=vZco!XL?}dCzRpa7p5R7LSA;^&-M>_zIV zLl&=3LI1|K%i>p*Dg&6y4d*RKACvd6mv&m6H9ZY#;g=3k7;cS$wb4-Y{ED#SeAs%ExJjnDU2XWZRlgzewlKsg|&d@zcp_x2M*l!!hy{Xg~ zXuL}U34RWbph2-&A(A~u-osueKiS2RaYg-qS4CZG0h{Nxi6?On8JR^KH7d; zS!$rW&*0jS!U)%Lr2;Y}Z`pBhQDHFEvXzvPFXx1lIpIc9IGzJa ztmHdJz?Fid~B^hm)z2(EaNeFm_b{RQ7{Li5+?Jhtw%yo~RD1;>-0qb$7!LxtBZ z?;U1&FA~m$eESAtD4lOpK%d%{(U1Q7PKcRZ7T&T9fU?N|S)UoV&J8Ool$CmiDA_z8 z$Fj#{vVXO*|Fp7$TcJ-uVNt7p0Awwmy2Y5D+Bqgu5Mvw22@PD4206SzIyHFFa=(Z5 z{vM&%PHTb%@8?AB599ay#=f8<%N&rr$j=P^>`)WPeeBZ}W1bX7ArFNyGz$?x zZ($?K{}pft+=-w2;dOY!)Q2D?(S_eYci{sZ5I#gZ_gfe%{0=4wzlYg>!beak`~j8= zAH!n^-!WA<$eFwag(Vv_ zIl8+Bh;%n7C5^NQ2#SD6H=>eK!heAJeg41w-i>$ndG2%1IrrTAvhBAWL1=-aWxYe! zw_HJD*^ypzPBwWiy&dI@&F2R$#|y_>&1P~uU>(-axtC*!=ikl`biO#7&%Z|74s9pD zBn|JYTFV>k1_>gD6`3Ja^*_65&Z!aXY4*c+;Lo~X75CYdUt0A^S~Os#Co1}YB#%jV z15%VkYTL%SQc#-|Rst`ozw`;YF*|z5^8pLD zdO^~&7@AdsMnqH0M{ks$z;rU<_ZzJdp4STE-%#Vf5m*yEm&)yZ9XI^abWJCdO3e~9 zxa(9Rn3h0b+lY4+hNi1Q zysz%=c>a1HKy@Mo1n?%i0pgmWJPEGcAKtiC26rStW}J$(l9ta3-w4VbuCzkhOlJsB z@o!u3X`vqr`ic;Bhh!^(=8?SkuNY0O$$~y(?nZoZZA6;P=3i|eQAk{-=3f;8$Emu> z=zVWM-uAx2qA3|+oXD7;+CJ_Mb9t#dxAdkOnSo6!8^cH~+L0-~Jo7V#H2Tdi{9o~G zCz6wRR%ow>^+Qi%Ckdk=G}0R^*2Pu%9qNMM!C;066vZZX7d-ZuINZUH$#k>X_H+Rw zq>L{!EEBMsoJuvNAS7mMkAX5fgU18JwV&ns_lqfz@I>fo4UXWXRD!GW7M11M z%I2#^su$b}O>8niXBkp)Lx8tm+=U|**`zK17C@GeoO)~|E zJ{}w*WKn~k+Cryr*FKNI=>qJk9$|?xvkrYJUviNrQM0{G2mEG|BMY4uU=pV%wu2CBdXt< zOtY7U!K27I9zPYCjkUnZ<0-CUKO~@pHT={dfD`(ipLsh(9|0#xlX|JXAmh2D+dOCi z-t`{H8Djm{;Hp?^_RqrvMAE7j@T0!fKi)>Gbcb+XRD<5{V^t0qxvti>Vhn7~)E)|| z;sm#mS?|Hn_Om6kEFt@@ft@)GJ3(d9Po-m?5sc$~k3_z%!TKJeyY_1Z>uJu6XR!q} zgc!+#t2=hXb2cm7(6Ws46XWyrck<#4Bqca0jLUBruj7nw(JuoL>grR}}ow;KFp3W|KEc zQ0LqP+r8o(+o6vond*kr`I3WleG1WQ_SN(Zx_gDVlzqM)wzzH(cD`-QqxJQTBi6Ng zthA?7mcA5Rz69QvVz}$ef?Eb_wkc_2F9$4es~5m}Y62R5c+2vUp8C>xa_8~MS_^M; zb`%1`6$0bs_mZNg7%IT|`9&$0jTf@rmnhHm5#t`0EBaqtKq#mJAE7E?Erb8b&NC_?fG{}*H3zpHkCQ&@ZOI>qh z#JokvIrKz^=cZfhF;6+CK(VLKDPa;&)OJZNGZvbmRBczx@el`xW7%)LN?~lGLaUca zRK-PVrAg^-dY+7RlQSM3{|xiZ4#j(`fF$~LgdMg%D%BChTa5{B$!B9L+NRX`-AmRP zGGS*1je64kpOki!>FkqfyanuYEwOq;(}sM@Jd&GHo2p-%!?dC6tZs9D&FK1Gh6 zw|ca^4En_*^eYwIgw-gX%yjx_z@xDK14g70i1d^tw4gR98?mL-j303*b2hg)*Ky~Q z^|?cOU>jv9m8u3-A8QYIox0O6(NOx+!W!qKZ_rT|*gU zX5O?0)XwMNG;j|YjYUD#TM>wV#N#`A#5XRG|$S9ASzqWdc$w=s`GsEp9Njy4EQ%PFgt+<$2hSGD zC!`M8;a`DQ+z6U(xt}2T#pyc1%2i)$xDhNO%xSd^C;~{n9Hxq}j-9LbfthbS53pu@ zB%4>!C%2B+<#L})UmI8IexPPQRAljq6T^_LTX5s>h;I4Ye z;9Y`$%=>V?)q|<=+zuE>Q}#C{Qdf2Ikri5#-2iTxAuUa!KtM$Kl`lywu$w z7>ethQ@^MbOAky7KN1Z;k`6x#2tNXbA5lMIq}gJEV?W^_Lkz^&)4b7@plgw{YYw6G{ zj>jk#1;69FL)8^*?2iSn#L)-l+_h#*HI2xg<}lAYA+AX_jcT4InPTlivQ_8_MJX|R zgElSV7Ii`{O5cTnP=c|7wkz|@>~eq#@4PG=Q!@GOlu}GMo~jp%QqIamDe~E?9={9E?mh*F#aLQ=nh9 zXfBdG88qcid5e*gdRxSF!5`7oPLff3fC_yL#BXH2B?P45(Xf7H*_0KK_!cHhZcKIP ziM}CF6^{i(E%&KisqXb7*3Z_pd~1PwaqP8jr@^Kk9Iq<7-u0)VD*L8*_vwTyfYJ}f z6uKCtV-$`6=qzTvgJ1)eV_O84R=0VQg2k#u8(d;4dR?w8#@6-LloEcG_BTgA)vkJ; zlzKqeFz`kEwzzY2ga~}6>R%qkkA1PpJ`;lHoEP0yoJm3QUO*yG6NQBti}Lbi^h%3b zrKDhk`(^`u*CPZMhE=3R&EKM*vQ4&qIM^v|HNk3nfv}PJJrP)u78l*tgQ{fIQsvn+ z>x4oXlKS0YEfxQwfnD2YPRAjRFEL}`NLFLR5;M<|jHOwK)mhgej^#7uNX#gv6+Dm= z2gxI#9BadB_=mnIQ~MA7yrf_@$wQNHY#n%^Bk2+7PoFtHi&wIvAkSZ`30F=||5y>u z_X3Zc5I;mzLh)i2|)VzC89eX|%Do>54;hQoxyBH`2lS zJB8?|WS-W7z)m#y(N>x60EGCA@1eU#Uo@DPQIt2YR&Xz*?2;+?!#V=899X@`95<>N zYkLMxJZixgRft$!Hoe=rWsFHjl}*}g+SYQEgQ^)!Npg&rAlza4mg%AxwnNsFuW+mU zElU*yzH^vKM3&rxo<@Vm_KPDKd8vC0tn5zEPqzm1kFYEIB5K#@IHb)k3AIm3o(TbZ zT+(TzRlZcj_tjksz7DJ9#I_eUap8+VFtd6h5;g-4J#fC?KPrpHhQ1>|2pbR!(}ptS={CsNmgeo2 z-KIMaUgv~kH-!w~wp|lrpQ6$3J+Bb!5iZgC+C`>sK($mOx(mgZ(ifpP$-supD-Z=0 zeUFUD5C4-iai3gZlDSnjP=L$95m}VHSbuw$-#Q=1)#jMBDSHsHo@L&y`t zWBW}ZmFHGgR>O4m|NyGJW7o^RNcn|f4!$6jweKyMMUV+p!=>7tAeT~KW#?F#4W{6AF>*z_(TxMMGB4} z#HWrd#(D!Ba2uz}c)z&lODJ9>x@kp6q1La&jDy*3NeSv|W3@h_G8hGsHKwWy8jTL8 z>a=D@kFx5t3Pmd-H97EC8SQE|q*}fMmiI+|tyN0znpIS4q5eY5*5p*aqPV!P|3mgO z`p~n}G3gR{p`#Mun@lC#eqBy!AsDbc)5o34PX)`}xLYf>*-fZw8hZVTW20q{^U=i8 z%QHkv<{6cVJ}{@*RI9uTjT8E`bAUVxPwaZVro0IG%Mf(LzPivxV zo04eM`$3UGJgnqOXl-(st>taj$IlX5bR&#@NPb^&^3771y4#EbE+lC|-H!sS5f)W+ zinHq#h^B-Z<;0$p$m&B24nCsm=X$yvyBLV@y&K{tpdBC~eU8!4=ekaEJ#9~BM~@mx zwe{g6@rc}PE*kqLDevHR@t1ZuFyE=P)ufz8fiWshy0ZCe52o>1zNtinZ?Ghxnhl?@>K}~T;!hzibJ&c}Dp-&G)y<=twMYOkB`Eo!CD4#A9OD{Z{ZIj%MQ)|FuCGf&|?jjc1?ZS z%`8LwJm-hvapO^)G6}hi82S_m^(@&NPm;6|vlgDmovpw4XzI?Q!o%Ughmjn>K8sxE z326%YNh@))=arq&@nEE=jr?`DYZR+XM@~auQ1%VIie3;L-B^90yf29!%XiA+!{y{s&@}$i+@}(`s>#-A(BvBsIJ0hPl1cVUzM` z&3{D4em#al0|^{n$Sk6z{qa`Av_;hoe2}%Gr+d8&$IizV0sC_a?1Qs-t7i&*O_)E? zdm?YnjhUr$X73mJUea8inzL9qP8D0` zrG)^?h2$$L_- zK1nAo0K#-QHmdihoKO1>9cWlJo{#%w1$Y)`G;YbW2xTVdfitsc4WFdi#V8ihZJjsD z`o8%7JULR$ocD6{)0=N|xzU{BEhFMBuS|)9Zz3jcoFm^8;yxCkJXNl7P_VkSDUm{H z0;5c?@+$X9=4f&zTQ}0{Nkzw}hK*}_B$t{4;CvKC%a5l>nr8B(5>J79LKR}$MQ6F6 z?BhUhsW<21&%j-Ul${Zizm#M$XL3>$tQyCHe)8DMmhmn`bC&eub(9u?Qb;0YSP}-S z*T<_To|m>y!6i|O;k}1H<(56iPmg|E*x?6fRsC2Re9BB%XNs2g!O^_Tk1W0VlT3mi z&9e{AE00|x-|ab<<3TkgIM_qQ^nb545jrKsvBY>8)VVPT`!t{6%;JEN-(N=!Eaf> z+7JMb70T!LN1Zjc^li=0s5#ZCIj>U7QMb)L_o_IDaSscDj!A*XqQGP6qjYzGjXX)O ziYNRQ1aTbfh&aB_q@&aQk)8A9S7nsa>*sU(L;&D<`QXH18l2j8lJkP!F z?z|U>s~0lE%v&WF&dHW_JVH0|^|z#>hY&TIVd?n7gJ6Rd)Nu*SELY4du#etgwvGk< z_XAG&mBK@i+l9yHy{`E4gRYO?-7#-qxQ$WYT_;U}hv7H}=;ly|W)r%jayT*g5Y`K~ z%td)y{9Nq=3YcFMbgs7@=~gZ7b{>6<2>0?1V}1N|#Qk(REZ4U-=CF<@C`Rb3FE<-8bde9?8hXs&rpHE!iKlMlSO?Gbk-12Rm*%P~H%B{l@ zd%nj5VVs-nJD0QI5(7h)2%d!Q|bXL~0)L%h&Xhd5+ZaZe*Z!<6-F-NAC3Y zGO1?ANbB-97F%jj?Tar8U!a#~Q|nn`*ACGdCV&-R5r^4%9k9s^;FmT92TET9eUR|f z3~M|0Yh=pli)B2MDLpkkwrHgA1@|;yrQ*=D5x4H%n~;INH?cECyeU@R+aw-ztpB`s zS~o(FO)8;6sxy;Z$=qJ)vJf_N>;@9TNv(rg&eP%*qWUJ%%;FlIt0T^ z9*ln=Z;4`N{|mis@s+jFLW4C4cd47Km_U)Ai#TR`A5F%$tCCE-)K^Br2f?Q>W0m`@ z19L{UvaT`LmFEFY#CDC2%O<9@EA0jCvoaVEjLi&ru~5xk!TNnZt7YftP{GS6}tZ$V(HnQ1Ywx*h6m2tcvzp$G)Q?b<9^-k@C8+ zlz2imYF=EgFV11+N?w4Q$&kG8S`dyu^@-18U5Yq)l4sa_!Iown=*3K2?4g%IrGvbL%5BgO86>(52!y;Aq zEDT!hBbv?8b)Thelw;)#(bI4()aM-B|lqv>{c_ z!B!TsF;Z}>gwECACGaS%@?I28;X5+pf7VHV$|jn4qT;;`x#Br~yd{CBhXT$uN?5*f z@>#rT^88@lxj(f7P|Li$NhWd?bz)eEC5fRIm4<{U$4PQP1JRix* zVks2@_QQds2J|Rxt0y7jokqZsawC6ds;F$sblWwV; zua9!R%IAEAp2+0{#PMFB+aIw_Z}^vtikFPSAWXm!Z!ZyV!$6Nj8&YRVeYUrP$hpNM zMZWaOaOfG~-Oes6!YVi{N+mQP#FajSla z|8$ml+sNz$H`}ItwEO+_?6q1~V~-yV3yx|fKQZ{Q`&I9umaPa_|1)SUVD~4dcGn53 z{WqHG3}>}$Z6k6-CvMYYU+>2CkI-Kl_?#tCK<=Y$-f>av*IXR+@-VV|jzQ$T=2vaq zd7rK=6K%YGH3K_WFVKbQ{u!GEiHqYX@-BOHqC&7F7v>#;I&4$Iq5nvdbhlqsK}~JKVx>*U;>Nx}qIe;i_-%Xp z!GdC)3W$f325cRra<;?9eL8--_olEu*uWlgf5PcsXQ^2JoYVJUuWt-$k89AA)C6G>q*n;f4Xj+d=~jD%0MUPktm ziLIutBUSAzd!<9vDg?QY*o3OBR{6wd*gfW((oTfcQ!vo_sZ?~Wu*=Yn)N!)m`T&(A z`)>T_H%>I4ow(0dQtQ&1)u!zYi+*IM6Rlrma#swA8+W1$Dt%p0hD0QHREalU^*u?F zA@;|do;SqJjVcKK1rs~}W-40@XmYp^CWicLUfB3`o||CTk#1j;*G{Q2il8l3ta~%R zUG=;C4`txm1yNE$N6(Zm#%!;zlhT*6#|XcVi|3YyZDB3@UaI%2g8bpPcMWpO(T3SB zJqNDm39<%|7@T;3FX2Mw{cyPw^9`+Y%+}m~aKs3m>%!WQ0FM_Afsup@IPEsq_S(e6bBCElD=p#0+#lQ=oaMRPm@UAG9{wp`` zE5Kz_(hBMs7wXWyTMN!m!=wfEm68af`Yifb_kxtcnw!R~{q%>sgF*AfS;(hR2g!c0 zz{GP3lZxZw7vB_iODL)Oj%aoB5$DTDQ!^T0y)u4vB1F1In^0@?KET#Lg?E|C{*1~# z2rM~$jfm&eR4c^)Ahz`Iav^L zj)Ep=Ttg0K@+TXzLi9p^vKDUU#Jl(J!GuiH7y%$!_bNE0!!>8hIEXa+WUwfCBy}Fr z3txP%O`)m$?S9H7^r6Eu&J==v)+Ly&W9+uXr#?Dpq_x%X3%jUA4lBH>U358q%DktU z8q%tAU80mrRUz$(O(EPdA?c~fh>6MU?px;r1D>RPLUp}Db)vCCE_|-3vAzjR;4=BK zRHm{a|KPs&k{e=JMW$1kkAH!6JqV_fpS)}tTjw*-YiloqVRy4#%dh0IhP73s3Dc4F zttM%jVtVIy{XtL#S76d~f#O<)^P;`?9I$fDs<0gfEu|pCRUfH+KCdwt)sHBS>T8dy zvR2G@i!1YVjP_GWu^9Hds{De3KJciY4gG$0Hmi}|PE{Ip^^NUL+0hF*sk3W4o37fh z4#lnAT9gB4UH4V&Yy(-pGu|fDLVra(>>{&c9)kWACUrjRQXhPHx*5SA`o2b1p)wSWPC*;FrrCr!u=wL-JM^@_0WVXE`-e;axxR851k}VwFL$~*q?Wt4o z;HwwnOF!*QRWS3-VxF0x5e1MlzZMdF>{&>YU%$=FkVqlmvP0fOGwILE!4kvnV^sh) zA?Kzn^d2TCs?!gD{p9d)ER_;0GmeqIhse}IRNMd9N<%$Wj`U~>5a`8QopweO9WP=g zwuUA;$lJW#8a(JKhvrV=5WOrV-b03bPBs<8F|2C~mpk#eAF~^IV)N;~Ni19?FCJX% z3UeCEX#^6S$0s}WT-Tg({Mz@tY&lzspY0F=gbCYQKI)NMHhbd~WlZOR%dUJ>M^rl( zYxXwmtXpd(TfC|jaN5g=ewn7gvXxZ8%Ui%J8kyS7ZZXH}!f)?rp`C==HYFHk z!`NM|ck$xXlvlpMTpfp$;EnyZypS`h$3ojy=3(XmH&IfO!;7R`m%8YQAMwRp8$>EM ztkXN{3dImjOVKD|OB#oGFuh>*Ez#wVZ41F=pyjzAByY6`N-=Iw*3*1=@`!8L*cK1%VMbM+Rue`VU(*VV9-O zmsM{7@b8=tpbs?KxPq_Aba1ja4l$3D$jG03L?1)Xe(w`K&6`ud#5J~R$Dkc?B|f*- zy`2wQH5+hm2_tfg*oS@zQ9V%E|2i{!i}jSvGsD@C@@m7NNPUUk!+^)IY6kj^T%7>b z9Ir3!?>?B=O@jjy^d?*okHtoS{(O?z=seB6&Shn2Cu-|o&0j73^mg#n-wAl?y_gkb>RBjNMZAdz-a6$1NukRyw-t-2+}TX4 zcBOJOfaPYzMT;Yss|#9`AvyFo9d)(Wq>kRE1Gx)J$AgRY;H62$<)gp5;;lL2$Qb9N z4!|M0Q%M_#V?Gt-k3I;ScBe8`5xLCFr$I?$l(5Z^8L0xe=+c|9h|V8*ZonJX>}xFy zb6FlQj9}M4iPAS(-V6IsBdv56=2YFYj#^eaVJ?iKM?2y+lkqa{>)5>J&hWFs9s>cD zX zqei=8kUV&>H}J!^XDw+*6zynyp9l1dCh6MNMmO*MaP64lhWiyYHj!wzt(-}fIvI{o z7E@yn))8zI-=7h@KR4E@f6_DhM?IpD{Q4K^tfP-$_Ojr zsq9PJyQ9O7nw#lebY_q+s*qjCZD@72BML!u<0#hVku3MmVXN8WVa#@KMo;ykak10= z7p^Mmg;<7#ExA7{*|uYmw_oGDXCiXeS&{`e#PIU^O>}no_`2^)t1w{gOUHEuA1|9!eedTq+TlZR-j-}EC*$w1Tj94a6RywlWNz-?2&2Q&u!8z^Le9OMO^5Y)DVso8rA53(qwvrIkD!WAut% zr#Y*MIf}IEh2_k&6gn*Kg?pW3m2(O?L;k|?EV5cuWC9n737QQeCGh@Iehmm#u>R6l z=B{YZBKK}cThnuBK0WpdM|U8adHap=ebqeZ*;6r*r%ZGOIf0M(Q{Tnjyava#13!oI zV$2zs!+BD%WhE4BOLmy&7567P7^1l^kj^1YZ)C-d(O3p=zKWVc#-78z7(S1ySP@7i z7|0WuYT8!w#TidOz$_9wrT7r`BGD*~d;!_WAzUEyU!A-x;l{xTP4jR*20U4zOdPJw-nNSi+3W*3+PJht2L!V!I26Izm^|K?MA?{(*%M$dN!!KkYf$L5_F{hFBX3&3W6HrtbdAXFzhixV+u}io z2A4Ur>qWn}qkH1v2gfP=+*EE`a_RQ}RdI)ek$~9m%eOUM}AQTiJ!Ur)C3J@`{lkOgZ z_)~yb9-6D}nn#%)n)fI`4EN&C&=~^$6q=X#qYV995|YCK;i3eo-m~riXodOv8}skK z001W7K8Q{F{po2;hx)bE;lMO(J(S z&Pky)&h`H3Q(yEjgxh;ONCORs=H7VTX$HjwMgYJl^Zy%9TlR;J3z=gFVc*k9GT$2- zLe*bE)s6m9H=q1N=lYKWgS3joCU*tb|0j|>GY<}!Wx`-VSm;2s_wwHPm+u0g@%aLE z`rr7#e0q@A;6YELn5TnG(EXXmB|7Llp3naW#$P0YsM7y|P3WQUx6cpo%@_RpX@1Cn zw|9`oj|ZqB7dw4=aOd5hT;lxe>Rm4Fp+XuNK-{bv$}m7&5Oi)35dn7#0NA>s zi3;r^2(|v-92G{p>!C_<$I2Z#F)xM2WD*)e&A+;LIZzB~U;wc}?n7e42%>(lGK3Lo zWh>5|04rpa5k&jYe0bMvNcGSRO)y+Y5Dg63edr$ueoRn88SMjM@e+mAwp985;xgnhk{gz*1s^vOcQ*%lhr~)E(w%_%B8bkz@xwShFu=BNspp z05m!R01E$Dvuys*sh1tZ{4mZpcBob#_`Vssv7keiL!iOGThtG@8wV7J4SVPY4L;`m z@_N9Q?lATA2Mn4B1pk$;s+>?vsr>;9xP|s}=zzidFX6E}!fxloz`s{HB7}nrMD!r8 z!Ue^9d;bp14C&_jW7NbQMnCX?L9+!H5-$Kkh46EOs2(~SaYG3c!w-a|Q5d5CvTXbg zb6t4Az>6^4|C#Z2!qc~B&^4h3&331M0@uFuz=sBo5YoZ};(r+D2M<(BXzc-mdWrI% z%}23=kcR`B5=#HGn~a}7@S%n?K}7#LwZIG2a{BdGi{aibs=HMPHqiN|3C%_6NF8%Ti=Nb6pagFQ490G*U_=md-W!@9NqJ-V3i zAK%S{p>F8A0MiN?XdwB*AkP1Bfbp-yodYIrKnRv6kO@L0^4oz-Ls@YI$yZ$NiiXQ*jB|8Ow_f9rk{0TKL<4eWopcQ(j{K&K=Y$N>=- z{j*rqM4@h^{b_fM1d>DApy0g_wg(F>G7R*;0q9-huh5-=*P*|KaK%9E_uRT1av>m; zR|mcS8)2{T-&~a#AU=dr{I_@(cHrWl|ALhM5ub~=@1T6o{hesyP%9;(9)j&A4r0H* z{1EYx$=X0?wGKLG9REnPK`g{TbdWW1=zg&dT_TX%I|0Y|2Z4rKAPyvz1%!QXd9(nD zSk+x_KraK*|ELTm{heJjNM1U$(})Dr`a8k2IM=*XX!+Hbl>a~ZCR6V^aY5)M|7_Nx zlF+f6i-3^6a-cZde_v_ul=>1Z^vIw=13^tt{b%@)iofQv{L}4j(f_BeD-3|`pKeH1 uK!`;mkP%`l^~ZuhDd^@wgA2))0#QB$wCk=#7Z)-w1yX|lfD6qm!2bbucYUz{ 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 4bf90e0a037cf5312b8f6e3aed76258c6fbcf061 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Tue, 5 Mar 2024 13:09:25 +0530 Subject: [PATCH 139/148] 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 --- CHANGELOG.md | 11 ++++++++ build.gradle | 2 +- pluginInterfaceSupported.json | 2 +- .../supertokens/storage/postgresql/Start.java | 15 ++++++++--- .../postgresql/queries/UserRolesQueries.java | 18 ++++++++++--- .../postgresql/test/AccountLinkingTests.java | 4 +-- .../postgresql/test/DbConnectionPoolTest.java | 9 ++++--- .../test/multitenancy/StorageLayerTest.java | 6 ++--- .../TestUserPoolIdChangeBehaviour.java | 27 ++++++++++--------- 9 files changed, 63 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f3aa6c8..8508712a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [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/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 86a7e876..796d3027 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -1930,7 +1930,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); @@ -1938,9 +1938,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(); } @@ -2012,6 +2009,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 4f26a52c..580cc049 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/AccountLinkingTests.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/AccountLinkingTests.java @@ -97,7 +97,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 { @@ -135,7 +135,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 cdf0c28c..c89a5ac4 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/multitenancy/StorageLayerTest.java b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java index afce4e11..7bca0a99 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 @@ -788,7 +788,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) { @@ -801,7 +801,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) { @@ -830,7 +830,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 5a1d7a1f..bc5a791e 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; @@ -86,13 +87,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( @@ -103,12 +104,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); @@ -130,13 +132,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( @@ -153,12 +155,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 7a9cc50b974e0d586e1af14e7e632a87e0784a78 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 5 Mar 2024 13:40:00 +0530 Subject: [PATCH 140/148] adding dev-v6.0.0 tag to this commit to ensure building --- ...-5.0.8.jar => postgresql-plugin-6.0.0.jar} | Bin 213545 -> 213610 bytes 1 file changed, 0 insertions(+), 0 deletions(-) rename jar/{postgresql-plugin-5.0.8.jar => postgresql-plugin-6.0.0.jar} (79%) 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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 From 85b4e525db8d17f1f1960abff2696664d82407f2 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Thu, 7 Mar 2024 17:54:15 +0530 Subject: [PATCH 141/148] 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 zcmY&eb95cu*Uk-YY`d|2W81dvG-}kKx3S&Wwrw_SY};vU^?QHqU*D`XbDp(z_Sv&$ z)}H4~SrGDR5HhlgJQOqn1Oyx$#JhuKJTf)Z{{}8O>i-5Ei2n}A|6Tq&p#D2;omikC z{=Y*p<6nmb(Eo-+B!KY$#s(!o1@!+w3b6qsM?6-V2CVvsfHdp+t$9#G5 zGniOU<2@4oqic0I-WLjN=r;S;G9ey)hWmiDef4+jHNb{>&+b9mWp;Wk}8Rf zTwiKF;)=j%5SVmBYSu#Jv2m7O$E{&HkXR@Otq?@_sq`K^gm@d6)Ay$(d(ozK&D7D6 zmeXMweEWm)C?hlIK4Z1wffv+PUZtE%bhS z6>|5=90Ja>^@I+pX+*tBdzr^NlV^bCjULpVKZPDku7mQH_@gwyVYV=tQ74LUEmMpg z!~RYbSZpIdr~6X3`fV=7TY26IynMYFO>^b)24ogoWc;g~`2s{Yq=SR8|EM#bEN_$m z!`Hu|pLu+)Q%0=&jgSejc1>e+rd7y(rB%!h;f%kRStD9QcIHZozdE}YqNf5gI{`9b zj%c?7vNUi!?JiqcyTcv11LEU_LgR&SLy$!H$1^#0KXcCFh~18vq9WdO!Qm>>j97pA zt{gx{@yk4wzIT}Q8T}%ifVwIu`)R$xqVz~SF0Z(?+ha&|3Sx*}>Jk#@Mur$|x5!!N zi2M|#GOicdc@0pX%XrT4j>_L>D|Slz`>fbT5zlfyDGa?M2tc5! zow!Cvefpuh`#vhS%Fa{TJ8-2KWWYz|{Xj9VlpW1mIpe~DI%&n;qTjfd_T}#E92z`xM&+q!A#P3I;aUq}SShLC@)U1wtrB3m+_k_^n zYls1W`z$hrH*dpHuaP<$Bwtvc1(~!yG-U(TJ^5XZj6i6)KFb%3kW$r3OI-t8!OL2n z!x5($$Ze&@zo(o;V6#}=C}PIpQwK2%9o^~{=+r{IiI&5r}I8P=+ZZM_G z`GR>?2d)I%g!lYMlQTSwZz2vQmZn?co#XNV<+pD+v57~A7@^^jq*q`>bt3~`4)Yt_ zZ8+=Faeo8_AMuCMw3TS?1jgNdd@Tju9dd_ZUg>+~=pMlu@2;GEbDKM7z>$})#X^_P zEtFVu&hH`3(PA>tAVK_y^5LQ;+`_0;hQ2oWQ_LLrv2xHV9j172#njPI6t^afHZkQ9 znp2pim*&&21$}Xvjqe`Lw2`egEaoa4koJUXCH5mrt2E&7b%NN z4i-h_G1s7Eonh*ln8QU&xM)@T1V`k#j!BdJH@y!hunuN+xFOg>N9k!#Oh{l2#NzbLWIM=f zqiWn7D{?<21DW*jiwD*s^-jTzQL@iw_ZmXroLX1S=}SUXq>2qkEv9iZ;aN^Lr4zaO z%_GD*aL1#Qns|pFMPGt|KpVuGOEf@PH5X+`Z3A(*ea(mDt8!511M11f+uF;wpbT>1 z;fV0`>1gIlxJamysTR-m9D$5{oO;(94#T5Fd%w;wrIg`0W%PyY*b?&2xUHOx7T-78!(|W&8a~XmMQo#d zoEyU8B4Nmnmu!SpGar3HA>vvUir7n#&W44k>>2?g_bh-N-R*K306v;yp ztddZRZffodztfzZ2}dEjI*g{A*6p;v6-k*x{edo~WM^xLy;FEH*amIF0Z&y1HrvAL z;qvFE7;-l}l{gM}l>r44#U@j#C1e}-+sRm=_S>F}Vuh#B+i_9c71OZB7s5r*1V50W z)xAG-12Fie2~z;##FL`=v-Oi!dc&g>)^7a7Sy08*M#X*w1=iyHnjijme$H+8SBDEcvJB1X-1@!RGPcuA|sL%yA#N#q7_k7W~q5*`m0d@;A4z*B)8`ff#Fs z<&Y3xflvXW10?m`Q)+<$;>1DjLNgk5w#seS%$(T9Jno36%ZsxO3HoNoLSMFU43`Mo ztg?cV$cjDU!!<*NUPqV|wkRJN5n=dGOiZ@np8zD(CJKF8pdb0DefeN8V$@cKyj5RT z5uS{fQarOn;am6>;gWGT35HgEf zN%+FqG5O^&Wm=m7sZ+m%duH|Nu^@w(>9U+J@b)HAEolSz%gP*eIjXfJ_P5s!aD=nH zLrf^zucO?F!G5zRIp+Cb;hJ0+>{G)5Z(|i__yI6e7UP^I7Q=(Q!t^}?^Woj*h&MM~ zYgqU@yCj$(FOA6C;ObjaS*v6pfz-@rxQb$|PKORQWXFsAeEE7u>-r0o=|PMg<{(Eh zdyL+B6M16F=QD+y4Fs((&6lQKKG>CJjq9|b}{J4o{Q5Z5X(tg6!3nZx4S=!>t_0g}HKhtZU>9O2@AnJXMVVlzrm zk>-fl$cbAu2GOJim0&S$ukS!4e)zds8-pOPl>{z;E&Y=UX5QHwM#Uzch0mfGc| z+ZWvVNPpH8yd`iL3YNm;Y>Z!p zPk@IlN)I;4wI<#Vgpun*IGBc82(m?Q6v4bt^G+;={028Q=Oi@o4PZnGQ3SfBva>`l zB$bt62o{ut9D|mPQ7K(%V(+geC-cI2-NDeA-3zFt(}af@DL`fj6NIg{p6 zBzgTLp?NE)!@_x7L*Q1l*gXj2zi_4bqQo~Oj;tg%&l$^GEY-v33r<#Vw0m@AON`K6 ziw(I2cg9PMta_0F49T4O^+A#FZ7{Aj)ZZn6OMG(RwakzRBKcCG#7gH42x z%nmY|*Kg?=69>3oRv+drVQL|mcf1$v>VrrYU*J0@-1WCht~iYTyZ!f(jm>!UJH&Pe z)2Cujyl(-sABF@hL6trI7*Ir3#tPp?kw*g_x3!^Pxv%sAuk1!z5apI`eo-bzTpIhU z1PrcV>TM8;uYK!?jZIDN76Tn^b&QBU(~OpU;{wL#CXsQyVV+g9)CrfPq3^H6zMpyx z#Ts0XJ>&NqVRMI9mrXLh8vz}@O#1WiA-1-kjmR#2an@1{f^G>D7u&QuRMnN3c|(DH ztZx2bQ4w$aS@*z#vD^l^YzALx3`q!)`du^x?6}6pbB1=_YV2Af+7XPki39jt7u2jH zrq7r+fjOO@_eS!eDRPZK+cfJxfH4Lan2$hP0Bi2Nt~h%c-mA*sGA~Eegdv^A90Lvn za0&g9o3oW>NsVkx(+VGK-$?-yEpW?$_R4Gj7;qW9`Qy8WqR6j_sa>TyI1<tQk#iOXk{4}7|GXBLH;$5N|*sKOhSWl)XBMEuzOL+juoYAG!Xsixk0;)LA z-5;UCNn-;x~7!vl8yo3{S|ouoT}5bbOc2eM0` zze*FsISJZ;3ZO09f^*iEL`}ndl9TNCj`Za$DSW*>d3`mW-6fXl3j?_oJW&CpbP-a` zlpK-vNs`98*vl)$ownS25_=^**m8$Oz{bP|aCl!w3z3zHh$( zV3xm5=$W-l*%gymGFg&DvU)Lwyyc8k(it~?H+!@|#um`=fMBv_%RC;?)=YFp)_O?m zOGoI?hWy5TI3MDZi}u)0bSL4^@`V?;U1jynpakb|kFjp!a602r##~eiOfW?j%{<%6 z;Xj0$Xu~KWY2$_%yXby98b0@Sdl*2gszrw4kI&KN+c6%Ra4xDBSnvq$g0kHguB=OE8C1p z$U&Vkke)PnfK3}OPu&;4H?dR%k|{eS-h2tOC2N1V8ahTWd9V%}-`0U5nn7l73@M-m z;|MYOo8E9jZ8=+IovMw)uZEMM8ME2WAkb2uZ=(dIYtp65zzlLAZr;fa*Sy&GoB54aq~aQni+ti(?) z8HtqDbOjLO6jeaiFW-Owt-vPNf@#RgYBmQ~^W}XhQ`Z7AuEnwEZ}-;U+~`Ugj>Pzm znexQ7cs5T9pWW*n!3*KhfqHz`&9n^&4;#?ez@DO~XJW%O;oOduOY8c8w7CfGJPxi+ z9P3@j)h)!GC05vPiG7QWlpIE+ii2l_gL4^!XZnL@YaFBYhTfgsRpS+w>0gm*(0ju;2)j5qMP8L&OgTr-aTZ3 zm%`;|euP{I49eJA^vFAv5%1ce6zHwkX_5%ms0GXRa{y z%^&_l2XnmO;hEn}aTirzj&XQ%9AoErhnp-do`Vbjkk5jzHJCyj_?{CO4ZXUz z|EWvHefMG}g^!4L;jKqy*`~mah>tuNDKgtvH`_-5lfBE$^vF$i>iv4l$7@rHS^p&E zh(lqF&hCi)4wVlsIR-0MzlG?0&B6OB@=JjBWee;5GVH^&%~h<@S0l<-`IoQ39r%-# z<8X=O8LuyiF-8^X=psY?eY-;!mA-J%;*`JP6FS(xXYes^m-PRUt2(EyqEZbCH_k-= zBPdgPPymYm11+ei0IR>8Ws4U&0R1o3*%E~f$VL9EWuXAb{-rise5e5~|Ij5JpzW_+ zN(D1O1O0#GX(PK(v^Wd|gc}kBL`dSy22;uv0~AV1E)x`bivbHD;x8TBasmQ4BK!@j zCk(*)Cv%Yia3TJcl`8`hk^UkP1Aq(gFKRRcY?1s$$^HP5f77o70c6ns$|#coPn3TV zX$K(lpSHUjFb{?NznqeMH}voRBXrACa{U2pEz|t~+J7dmKLJHhf5}$qq0JRi z@~;En3#83I2L~{u)8FEzYz9Nh|5Y-EKwACf$y<^_A>IEWUB)$>SyKh_dz>axLBp87Ty*f$0(oe;rKpXnI*<9{BQ&wshUOg;+3Sb5A9SO-n-NB{lt`Df366mTsujOWI7JQ z15jwvPMcswS+NQMlW?5Q>|$%yaa~~vvXC!!SKG_e6JRXw-5JWq2ekFHUHCq_MU*dHzv%ambLZQKp{eOm^dtCz;V z-EnJBUi^MWUd1vSbGSn`hX^b$M+;+qnqNOf{wg+@7!v)H&uPq1#!R_jm^hW!5r2nA zHP9?|Fn7t$)5dn1xb)$*Yp_x`g})Ax6igW?JNc=bTVrV->102h^oo0lFJpt6r%z6} z=inX`$H=O)EVxgJK!nVpky_Tr#)>E^yI#@5IX_E4Uwu_$G$A_r04?7MXqjuOBjViJ zyYmhxB*9esM7mUH&Y22DRVq7~jMFl8fTHn4xF~fN1>o!xP!3cR8NCocv=Tav2E!}6 zBNT?4>%S$rj1$#<7Zont9aPUESu~z}eNYT?3mMN3!i z6uQ73@)KTl5vqp9zrRoc3x-!a^T;1&(&#nV@Mfnqd?SQ)&hKlC4jgp!*S95kuR(a9 zHF!{%^vo1+T}_Zrf8Z4=hT4@_ z8U+Mryu8i!Pr`|FRpDvuoH5>_M1g8;gU~h+Oj+jer>L!wiZZ1QiJZ>;^yiFWS#1Wi zlV|Hlg>?+Fs`PzD{)7bA&2tA4o%T=5ll+TGm9kK%@%&Kn_H0+VLQ8hr;9x6Ym*GWR zLmu7i=NMuHhPgiFm(_$HFPzUzTu$ig{SWs}oP`Wz>^#r|0a_mEOs#T;kUeTUYRJrioN{i&6UNSh55dgqw5GDaOy(%&@U|G?*fzG z88}-Js-@BpFyrS?p~>=b>+!Y4_vQcK4=+lVgq{k{;B19)ltiW!9$X=iNL&=w zYIr}BrBi8Z>HSTx4hDGcPDi{q@pV~ghqY9}l?-UL$_ICFw!(hHERweBv7x)#ke^1D z(lP%XVY)`-%mQ`yaw#1t2bC|G)7GQ?xyYSw9hM)o43GLrW7)lm|G#$uEh6+A;Fc-a z{63zux!|cetY3V(xStt$6+U=oRInJ8&tQ4FoM+n<*pHq#3pQnW(+ZXbElYcJIWgwT zATsOq9b~H{BE+)c*<;fn(%8Qlu|(70R6h1&5(vn$S_Qpff81a63PkY{U4b4|d_&$R zoj$veNzA|LB4-C4#Xov+2MrNaZGWnuO8@Vfo^PKqc_CQm>Ezk|d7N$FSd-!GgWre5 z*>)?Aesz-}2(HKwd+u8d0k*6qUE2OaZABMv83)wtA{z2Qc+CW>A7VlLQBK>-G54Ke z5#j5Ex-7r)LEa1$-bTX%*oSk>2bs!8f(4=bu>7AP6PtZYgF476CbE`svD2Ob zTzcQKKJ6>c!0>R`EIYx;z4}~vsWw#nXg5n|G5R_QyBe{icbt#Beqqih+_#KAwlYB@ z6+?B-n#$(p?&j)3l+e)4B1@_wY|UGorKuV{J>4-AkBBP?PoBi%r!3&s0C?2Ytqtjh zQ9k4oILmEqN9Tp%%uOC|F11%e@I3NK7U{5% z=t0s)kAoxi7gkak!I#>`THYwfzz5tdJ8qE6D3h`^zJj_gOZ2^!p9*P#WD+dKDPKv; zf?@Ea&Bd~71t+K8F9q{=o#L^2Q1XN;UX(lrxGZ24NegSf`Z0(gJ())zLEr;&+#H6D zLRHjann`Ao5M7B@IaFQ;9F6?xwDjZq_}&-qa2qI2t}tY&RcQYF=%7;Of?{z_uBf*= zqy3VWkhj~niHYpEu@D-P?@W#X2UO^rg$=%37=Bz@XnO$}WM6=U6GNpAl|w^yVnE9c z@a|^rt3o&EkJ0Xz6F4@V^oU1`$k?i!z#C3cbICz~+qn3+OmbNDPl61IaKW~|Gt?Uf zbr4rlH4o&FiNsx+R6B>D2N5@4NlDD?^32_j+1_gzYmOB*`i!EwmGk966nBk9AXL80 z7!Z~P;Wl8Qs-iPHWH|_h&MEU*fr)A#Y(jNh&QCaUF@H;P8koHxMAUq?)Xr^2*?(wT z`6KCspmVt^KCxP8Z14SZIeZu%)GQ~``R*n(amWnqBj<}XMhqgTIWCCu-(0@yVo5UwFYL!n}LT)inA^pm8{x znnCoY!;t2ye7x>K+avI}W3ns-c4=;$_c6e`Wsey zDsX73!pXJbOkdV{DwmX3V3dJ?%H&exh*l0cvM2|(>F!yuzTQYndA5a<4h4Jk0~$Gq z_o%6I>pl(Cc}etcoEslCmz)`RQmDmG_R@KV#CAu+3gi{d29+dY^+EvAS0XC_trcStCSn^=rUaxMz$~$%X=oZ>u)_Y+%0osn{)XGY{mU{16h<hc9? zbNvU@yF|jw?lz6@hhn|M9_3eI8$G+@0+`22JX&UdOcl1u&V8caQ!0H)#|J4j`Po!P z<=wdE45(j4&4uf8@Mtg8^|CfiV4t6p@O0sk3{gc*YRegRz^)|0^fKY3~L+UC-WCJ?9>1;>l|(?`t) z&Ge@hTI~%MW8^nu5^y9Q}h6)qfw0dX={uWOFyqOqSw0{a2&TUCxK=rGoqJg(p z>31YlaB#W`Go#pwL~MkN?$J^r%Z2zS%>A6t{)n2U@PR)KNhR7)RKs@B%BMFFUotk{ z5}0pUi)+M7E#oA`IKGa}^IjlN9^%yakjv?{tB^5DKC0^snS?}Ztw~@%EL*@0+xH`w zQUwYWf=dL0Ji@E6Y}cY2^V?u|B!I|tkG4vN*eT7;UMJ;iADqd$m9fD&=xJx>5up?d z>4huD1G0mZq0=VEUUYmOHc!wk)F6b4d%OG7?>V^}BPI%JM1(Djm=b;BT4uGPgKtIM350yd^9cB9ljAA@cNC+km~aB-@YR>T4OOf`Tvsl>Mbq#qy8?o3^gj|tDf)5bRu>sdOu;QvH1tvhH+#kl`C-LUMMO+bH`T?*P;Le zyZ}(P(4CtC#k*dz z)c~^^t$Hv%zrDD;+QG((O3zwq^%br*o5YG`&Fu{1W^UM{1VYy4G(b z;LN*TM<`*lrdoZWrU0;qmcaVUOOlfqGpeJYfe9OF}hJ zZ{gWp=6*$KM+(SJ8fH`2rHkp9hY|aMxBVg@Go5;aqS+cPs>wc}7kNEY>dvBSu4e&< z=_vU~*w?L&`AyLTwGm3cMwaZ;7Dd@KwjkK}2*2U~Q-&-M0d2OPodx~tqqfOof3L0_0IFiQL_J1t^ ziYYD=zwJ$nx8bu8cYadr>CvQ3hC`V};b$R6=*QVD3smfmR`?xb2HlBPDk zTDSt9yI+KS_cr}1;EKFHJu|;mo3-_XRrkXp%j(Bs3g-@Xf);&nv^1H`$hkd|(s})_ zAy23HEp1VaU~Y^WowF-Wc|UuA^_!og5Klv}o!b$yA;u&u81UtgE{jmL^ND+x5JgePZ-=0n-QRyXx zW`kmbdp=42@`fgCYA%wYmn4PFAs>fJ`!g4$96W`G6zR3t$I3U;yEU8m{Sq##$F~0{ zXwn3cZ#}SXSBwMQ*rq>41FWHz%P~_*TNisBc-Om>eiePi6j;_y;+E-Fd+??$_>G=d z7DkrqR*dqsg!)bXi93N&t<_xFSU+K7^H#Pws`gC@vm5tg+@w)*e=HD3VS_Eg&3;OY zvJ0gCB#QZsvW1F;2uh+Zx?@*N7#$R^deGM!mPn{CEjl5g_@gwv4(#dH0Ll~d3i=RG z0%zw7PpRFyDe90}6-t!_8r#qK2hu#u4sJ@6nKu83;Q4ye55G@$kNIkAR-F3NL?#sV zcyqQvkY_EYoDJ;@h)w%^Dp$F;3&lw#ob&RzHd)Lgcg`%E8U+hh)juV6uQi^+^GS|^ zKj2FkH41hp8(!jmE_i-`eyjuh3q90+7tV$9GYIQ*fDhKC7DizK$OSaIOzXEEhR=Tb zW=VMEE>xl&+cMpTK5VzkbEA7x;EO|e#9>IpqNMVZTrratha)vj>|G?0=DQ$JjBvtt zBX^_~=SX%%vF4$)@0XTK9A6^m4xbBxd6!QDUtye6VH(;N2Dn?O_)~ww#fWZ%w9PJv zVA4iaukh?CsK|?Qf?F-{1;i>joav=0UL(*8Yf3p{*k+dz zvJOlXCdDTykSyAN5~3s;HsKo9aJ{hWW$MY7$uv1Ps8;tYAwBcR={{I(jk6|l91TOj zNS&*uv%Rvr6YO@_DhYiJ8Q?`0$bcG-u7KYunck{XIo;B^18sT!G?1INGl93O$pI6^ zx8P>vpIFM0!m->qc8Sy)AwRHXw=pRquAcS9oi$vP!#{?ii-EH)BI4?$_3gq$XI`*R zrJYqxKnc@+P!Gt9OgW!P_#&WR73vh@TAHkojL9m`DeRKgDu9s6?k7FP2QCfnI~9kF zLK#O;G>{F#w?&i>pRy#wY!X(*RL)bN_;%wgS2GY>nd#mi;RZe(a26N7KkRuO(c$2^s#d!>8vNfGzqweI2mBAPV$)aJco za~uslnU?N_)0WuglF8ONnnSb67537Skx;;`yD`!r>K1myj;55EisJM0mh&3&I;C+3 zzZrEU#b>3;%sC8qM_j#FDFehrOoG8OGiV%qG)9pGnU))tVk?&C#T!_VP?2)`6Os@- zIDuqERfD!;vv7tYwWM$+DLiv?XNM{epaD@Z;hg8IaLT2JIsUg)snx0yxc49kmWdgs z$zlJX2M74J41FzpKn6&$j4s z6!P>sAP>oByE}Wub-8FMeL_i8(>ieMdR6%2G{rMp?BEo>>E7q%o-2I64ph>)p3h5z z#Jfse{h_&89d-SC`tn<)+k)wAERUcew=5P^yIpY!;ewIi>S49dxR!aVJ|AzUyvQ)9 z_PZqPh0n?4KrtTXchS^$Elg6fUm?=$7Q|uc;=Vup7sa^G{NAeRg|fZ4vuMFDYm{`1 zP7O$GvHqnI%4~S(E#K=XYul5Mlq?#suknHAM=#=v&jNGlmv?P9`s7`Q+&q5fV$T)p z&)r%z+=jjhS&Cu8HQ_8_EbJrA$?>O%G6okU#>-2bWlM%3DV%K8r8H(o1K$Sh!!b?6 zbt*k|F39c8cCIVD$OA+ANLDWH1{KB*R62$eqwZpu~{5>?`-NanqyPH;|{CBEr z_HmRjs~;A+!IXw?sj0Vgfi78RO;b*)=k`B)>~^eiIPk51=cGTeTyG)0NG7YLv;Eo3Cc32wT=z_W5+qyN zipb>L9OioEyoSA&_7jh#&1ncd62NF@#<0Oigw_~lwb~KfL0O4mh-`;KmQA#fozdPr zTXcT>lRugE!~>#>z8VGBxtGorA<3#28KvjScBvx>RGl49^xmEhBwL?bWMSmnacv#WVEQi{v2{CnyMTH6)5|Bk}6hSJ|rbw}+#;1eHZwxsUn9g-!L^7+) zLAH!_O*ws9D76mwgz#0pI6VEIfYrl+B>ScYhEQrr{0LU{T$(XEJ+wZOa2 z_0)=a?+5+*0V)%e?qZKI(}lK;E`({n>^yT<3Y`9!+qx7|6H77yLp&Mt2F7vtmf9#L z%2tpR(`DxZPWZq*1Ikmgg)44ty_I`N1u7UwFN6Y}NW(Afyy+U`6AL9@p3hJgFa+0_ z_R(_H6zM6Dyk~rAL<^@pcC%p88&&9~SJPT`J>7UaUs;kdzejSG1)L8%>q8VIFfF&M zC6*W30maFQ-i(>54P~F^ZPcnHO0v^@3NYj}{nfx!KyL7Ib*HK@W;qS+>YOW=Z5lNb zYRNIhVvw^zo-9L|l^;Tt3;TWDWDa!k{B?M`8-}~UvRyf&l}OEE{3*VSMS{lYs7W7S znT29;zG)bwOLIm&UL@;S7T+ghctKvnUD^L@=oAN5xTlH)ns&lF+}7x}96 zM&`~^5Q3LDnCp`rPVFx{?h_5*FioBkiFU)Lai`qEY=QTImBUI}Bf5kpUtr%DlwUA5 z_TIMgMvRV;Yr9)Au-=V}c(XBqD>r`nq>#F&$CAXbyEN2?ZhladSk2``Ebu9u@32#L?maEmqH>5O%ymP zt`w@U^dK?)?#EqY+C|u6tavEL?=X773fU>ysbR5btFm=z*rklh;~P~thPhy^6G?&( zV>9PFfFn4_SL~%LU$#cB=7bET>T*^$Y1LlNtrmI@kNyh2)Alw_>xNV#Z zqPr>-rq?4$dv;A`ywvjKq<>+yBOQF_b0YWyXADX_l-@Im0>ajCuCw z^=t>q2^4*Ftpdz6BZSdMnCKt9wGGV>Yq4}z?IfY_Rb20C(g8vmz4{)R9J^WTS>w7j z%@tzgh-6YuEor%d(_g7VW)KOsrbN1~Vfzz6~G423+C^%_=0_D^x&(U+5+ z^hVK0P<#S4D}ePUuVQNJ(Uq4*X;4IiGuO5s!W^`JX3vgY#j!!Nya$Qoz&fGh6QY(L= zJ0oSWG~g)v@2h~h*m8zfbv%D!z<*jnVR4`C&|CGt&X6yxd_FPOq?Q%{>FXmm?nVOc z4vb0{NE-IMiBI>{@FD=Sz3pCEp?$qUQp~0!4b;kA!Pl|B!#$5jTpZW@$`v>}Pm86Q zX_Ys@xC%p|%sey5Kj`torJN{FHe*?KHLMb>K#GGP(L;jU5usa0* zuqOYO#@lPCE%Uo^{%DeG5}Vh_tQTIa-}#5%8~pGds7`V$x)I zCt4#xk^!mHi2M`F!oH;yj{MT+l%fkQA)#=pV>SfyyXkmUU6g$KoH#EgHH8*vqL=j{ zJxzHqgS+Hg8uijR4F~OX7CL6EnrCeu{dPojX3nv#i{(v#t!v!0QPC)J{kQ<`2p$*D zp~q1rQ6qr}vqCgDy;IL|HWHmp@4ofWXWeLw@{!P95pJp7*8H{ZhCtDX!RKUz(zCR` zryX+7oX^Di5Kxp=rXKOGB<}LP!Lo;~D4Iz)n@L@_fCk83_|tStTMLktOMdNLnQ5*=&{24UM?cJWrwW_)RX6 zr6@fI(D)r(TlD{pk8 zwmB087Ky-5Eu6Dvjz{9vwo(CCYAl2E8Mvx!UjJK)f=VbNE#(Ocgz{AG>o}e)9W)W0 z?u+>wiJjUGl|-)1?J8}7waNN`8CQBkBJp2qL zKT-SJ*9z(_s;vnVQCvH;eA!puSn|&)5m-d*A`+w)UaaHtW}fU1w4n3!A=9YJ5xd3^O9#4P+UQ`!6G ze^^iz=O@NF(g#>8s}a=E&*h!+iE9TNNHGfQn2Kd4oqLWpKYiRU4slol#sK;97f^9= zlQ_P`as|wB{f8#yNMB<+wUfXVqp{OOn#FYBX)97BO49m%GU2iBoGP!woX_yjvOt7# z1K!`DWOEwC1N@u08;Yh-zENb%R#iUPSX1qh;u+$_jq7IKT) zF<{94h$Pw_p#DDeLuJ|5`0s~5;o1FIQPH|v6=VNJ9IQ)aJfsGqtT~ehwXFbyGE{FR z$q;Y^N~B-G;`d>)0^oh!x?_J0%JVJwIFjiD!sw0sg&f}{^bI?Z(O#ZH|1z$}bb8B` zLSo0(4^6Pdszm$)7NO&ycsTfx@w=+s)>cH#8!k{I*<*qvr=+ckx7kl6=}+QQYxI?W zCk{%`)FJ28nA*Xmx*+z@tl_*Y%c9aFhfcOaiRx?PkOz@Fd1cWpDJILe=JR>5O_DIj zY^_@fL|_8+#*CKWvhs3-v=Vxg8wIOQLT*NBQ4~&VWd%W%p7Ri(^~b_;!|FKln` z?ri+#vJ&w!mO$0GXZsYUQ&4CnGurvpvZ?9bT{Ci={>*+HS0h=QxwdhtiY~)gU8`f- z2Ca?k#6IcOWZ%|8+Y5;Ho`(Pqoa^|7e>Ls2mmfq8A-HXkskQ+>Z>b>ni4T?TJJ~v8HCC*i|0*Gd%XH zqN<{tPFF?CxFx?G$@t{=9q2I?v1tUAOq; zmd>ccthi+eS0yL$?TV0Ls+IkN=|nf(Jy6fEKz8vvCEtY%U@E+QqdyRYzY%K9{QEa2 zO00HlrM9`XFA^%J#J{=NXd*4iw6V$~BwfYbh6o{5*e5R=%vC9=?!+3m)8qEe+H|0_ z3a|VyYno$YtnMt!&(HZoRPyo8O~H^DH+^Avej+UPV%J)#{U+Jv2{%x1+@8E-^ah(R zq!ZuzgiKez)+Z7#rcp3Bb_l6qM&aAoCquE6Uir#Kft&;Q|L%9G@zRo{xs!mZ<^<`n> zSR%OL3?JUMVFHw8apC0n5R{)_T2!VoECff<^_vvH5wb>Qc#(s0E%1{+HkYYz?_#4O z3z<-!z(!|Zwr2z-edcTT7SZA~XlEqy%C#lL`u1cVe099%*^yvnp;Blqolv}>8=9g; zE4qfl0u4V`%XZG!xm^s+sH$zt8ZvJ&8|l5aYxjl?TL@C^+h4WJKx~4a6=^X2`U=B1rG|fqg~Uz^ zUDO`{b~P?66?M6Vk~x_ty}igka6fKuoDCp6uLf+{Y6na=Jp&76KI3X%FI#T3n|@Z| z&2c+cBjv)zo&W4ak<~PP^5yznbbUh^kfB}Tpw(eMRe|@8zLbELwej1z(81p|@bI(+ z;YbRE>g|sgc#A3+-M^xE7693Su*{5E)*w;|p2OlF;sC~3;Ir!JHVA3_UO&F)k2jlg z!=4U|^AKiDaQgi20dM&8)3v_A+u`6uC%r)#JuWM)bNiDOtosM;eqiI!vgQF|&Ax0Z zU!BDu(%}&BY!S{)w*Nwn_W?C4bpEwrC1v6RTdOstm=X;q{a8*-QnJ#ef^-yfDXpS4 zDoc~5X!8G9I_Ka%-f)XIwryMA*tTsuX>2sfHn!2&HX0j^jmCCkqrvU(-aGTzGdsKg z?!3G6>^bLMW1R~4QRY%ajI;P6Z%~^g_XPOv88>e$-x?nyhdo;W0cCW(uDOt)$Pj6Ln7e(fplY|zZXuc$E7_x{m`9>X7F;=8@2miL$VVRxON-zVK| z=2xf7`#1+A_c1b2Jun zG!J|>L)ZLOF68RxiGu6?(l6g8r4uxHY zO06{B8@{(}!FjOB=p1~(u|)y5@WWJLI{dj9vX{}w^z*{1O`sWC6EB78`>6IgQp zt7qqeaeOZuEFwcZM$dnCa_v{~)EXlekKeis_GXyF?oWuDa&F;~@YbMW7PYfm37hs! zwm+D@`N(YM%DYt2B)w%T{Q!~&hHODo=Wc^6O9}38kBJuW$m{8P!Zyp+-5t^EoDO)` zH{_%677-dN76WMIC))4m*w$-#Yzj+3!RE@!al}t3O`kp@Hi@6S?|sCYslp(hJrQEd zgyH4AldQL5?&t2BDWP|1r#om~ZJyGut-FDHW)it@=H3a{HRJKfDL_1k5`Or~msH14 zz#+CDcJPPp?+hCDgkGQEU)-WF!^aCm>C3*Px*c*@Q+B-hu~v-jWe1fg&&x0<%R|b` z&U=X-xK(;(+(UOcKaHpg__HH)3~Pn|u$cwhAF%K7#e;9lfOW~MHY*6eEA$#m>>U)%cj6V!Yo@({ae7ST_&`3~GlYOMwKRjwQxj1hb@X>7h9lD{g0EUuOFAw#McO?;ivpR1EwHBcGjSj#2jWErbcJT7+!86V_U{Q zNKy_#4x5sZ{it`a8lC1?Z89Vq4HjrDoyrJOah0R_7NY{l*dHC|Dp>=NFlz3cIXS)& ziKzc9_)mO(i3ClH%b!UxmLA?#&nrw(%4T=&hgc#k;Z; zZtR18e>DKA^qRx^Qx-erta5Kn_P4RP^iVU_4>wGBiS)|!f3{yr0ev-BzbsaldNyz# zO{9NaI&KGc=JIlN;8|~-H#E#pf8XfvK=vTze-JzjAJ$ft2tQ?c`uS5#`}oE9)E3sG zeTDo(=H-%DzXNeETrlPY211$%wSBMfB7(7uuFV%P3BreHJWae9dBC9g-G1SP%5u}r z_%~UdQXwls;LLQ&hP;pjp1+@twO5clI(v zX`2Ix?LR=~7Hrx4tNY}q7euzUQGm%)1c#?K%?A{M zm-p{Ntjs|M6JC%xR|W6z3ok-N7;yNAr1ZdM|13H;map0U7ETE>CL&Ze(`y^rc~SX? zd$XjVq$WfrzJ}_Jl*H`yGcAmYLBDBX>PM{K*>miQL({`@v=$RL@w~CgIgyIW?o*wE z!>M0!yl(>6tU!I%D+52^Y4En`qg3cID%iwAE0g+gno!_4yGZ?Uw&?!A@!+m0C=Ou2 zFR|lc4hhy41@F!7AWoqy9KHrCGT~SqH2EomwvSN`7mR+t zdFIwTpLS zIuKS_KiF*&5dAr%ZM!c@{Omx7!3DIogSKx@t4>etIGT+F+v|LZf-N`eH}hkET~s;l zbw+#gA(AaPkvg~7{DJ!u>A#eP8>7hc6+dOf+Nr?WX=(BB&yD}m{@e?rbOLzx&Sie( zRz};z1>S^9>jAissz+E!tcMa~eT;lVGpDtj7Uc{!xg zc2`O(<77~N(i|}8&JxUrGK4UInUGEf zqQ5BwlO=@afp47+3f}?O#Qx0T`xQA!!Mzzt^2<6DAD2!qf-nF|{hQ;jk6Rz$OUBy* z7{RVaz?I=UhU?42@)tpFmFqEqH%%n+kKtCVOK`0=m_@8k(hD*fmK5Tkp3ptTmA@ZFrGXYp}&cN}(qS2Kb|biLF0O?(!(h5*@&LV!51e^e!)Yf~K71{ygt zT{Wlx<0ojnF14VK*k`J1Ckfl)EH*Vd&=&XH7p*n8L37hh`#??jLHPYtRnl8)f}5Os zE$Q87ILQl87?6+oa(DXX-})WCRWIKyeU>+6ZX9>3P8_c)X1|6=u7Hg=FVCbW`4Wpv z0^{@@)~SIlL!1Zd3VS`FlPJnewQV781GMzGJJx`*ZT|P#-IBhgFx;AA1F~I>r-vvm z;#y|pBj8}pb4$E-YkbH^HIDS0!rQd2WZ)}AvGR4M7=Ru}O4JNjOvG>c7&ShUID7%( zb_?KV^uwgLmZ7~{Vo-UuBMvI@Q9Fv_;@QO;olG9$DxXPKxld~m6`#Y4%Amy3m`&s!QFyq$6sUvC6sz&l*JRwmM;9S z&>}UOFbzey(4OWl=}!SZblVH(aftdJXBGbJu-2J1`0ljxz&mY)6{uc`bi5uH%9t>> zqV#L9fmbrIceH`OtZcg&_78Z7!M$edz2;}V{kYGB)crVwmwtXPxH9TieK~}eW7HVb z3;?_865a?YlI*rq1N?DmC>8Sb2%rsjha^}#;COQLbKr)5caN*htW#Fh5Ur;CA^?%M;dHrud9EZ389Y#`uJb>)_HBg`R%>-hnZQ`sMSanj*g(g*Q! z9TLxHyvTW5D`5puI(OgpB=6WJ_`>1Z^1(!b9~Qhy!LhYs-&;dnw9fGu~U#Pui7?UYyZ8yHxq%;Oc=LYF@UhP zdtvY0OZ2fgiwqd8JL~fMHDVp$;nBk8U4=NoHhtQ^(@mXI{CI7HdOdl1y88=_HxGL{ z^%oKS+F1YbB5;b~_Q3FZLFm95AKuouZR>f%-p^6g;hnHYVlI(~fkhGx{2AFtajlg^ z|2?86AV}fBfLVMw5Dt_U35t{j0l{=C0~+Zwg!K^*fvMFj*+Y%}zg$fy2w;!vMV)yG z5V;%4oKYsiyX_&nhTwPHxbS-{igxU{paV=oZSY0LVJ2%9Ia_4&jEfQ;xa1#(Pm z>9^rui9i<0kD}*D`?y9C-3VL;`!uzPw>_|cq4oG*GYfRxLN6UE@rljp z{6-t@WZa)k_jqc?Bkn#lz!`r*?4eRuxZUptzkD#U1oj%#PsIUX;bxn4(T@R9<okJ%o))yBLqqws7}1)o%j$_;xXkK7(l&JEj=^id-q zs1t4=ScUX~uOw}e8_dJC*)L}mSA0w8K#^q})OF_9V}IRsAjXw0c@nM-Lt1cG zSWgzGz%z&tPETNu>d{Bd-3n4t2gJ0XqgTPpEf98jT44XInCVQXdj#a=3jMf%OXN~YD%rgSZ_A(^k2D@wSR(d z`SveK)vq5RmKL51Y?xuf<4IO%>*VhcE%bC_VAH;pBBXqAqQe?TkGnXND^at=qTLc= zRAIlL6P1kCI1Y`8KoZzSg9xm>erHY>eDei9#xHS5HQlzK96l0jTn-{~6B-?gOJ;H#-oM)B9CUr+wAbH__2@RVPdK!1 zFa#lv<<9lmbf=;`9iDjpgIdT3+Yesh{{f)&ug56T?pyn52BT1`G?YN79U0MleWVCt z7xtc`d}Bs@Rga+M7jKiym%XQhK}23?C#D)OWzkyoXROhP{<}?(FzkNcN2bL<$@=B z3ry3Vq1wdDEozZ8Wj|4yhE7;KqRf<5uP!@APY5z@SA%@Mj-%q|eDSvEvakm9rSi98Y_TFngFJd#V>a`jl%2coMw#6-^HVwA9!(#En%+LlYEKqrsk z+OWvs&5;*ZmtGP6>LoyvoKnK=!1KK_Evp$dvpzSozMz04?8wagid)Ge8PoBE8z zS>K4*o{%&KT2;Wd!_dx&ibubdhXCKr9)%Pv^9I}>rFoLx;SA&BP2oOjt@85MZ!++X z2qVR0H7MYhbb|T`RbJcM&>!~X4$`ak{uWiwQnHul;EyrYGnfI55a(C8g?^?#j7^qd zn~>N&F}I2k*VV@V4cdHMYm<&g6(`1QthE!N7H&DBq0Umv@UW}BOpUI(j2zB87+ZM9 zgYaB}vFG*~CAb;2LSgai0r51_LiGYzWZ4T+DYjs)8Q$R@3ERGc?r~WtMpCQ=oQy4cO-B}Yu?tztob|N zQ02L`**li;RZS~=dH$J`MJx|3ZtjPN*_xwf=D#dN`!1fPI#=i>;W64OE*BXQW&uty zoXs#KQslxqhy5wG6YaVL$NFN8eVkE>X|yYV3d#f1sRF=sgppS+*2zuqR4S-7x=;PZ zxXQ(lc|r^@@WY`p1E@LP6&jzKM1sjXNIjpsJdfZktDstGGyVFzK5o7EQgfTAZOtLxd1xhR{t#tbhPg}|C8(}cT5Ia|{F zSEykh2(&wW6EmSE10|!S(`^nB1Ug7JH$hlgo0-H(nOKp6*fGE{u-7Gm8YejJ?2y>= zBB4*nG)mzYG|t8;b@S&^ZJA}n@Wet2x>cf_GMzFvKpOW3rN0ge_ZTD8v%^#KnFQA` z7R*_Dy1}=5D4}HGJmlh~Q?ww`>v)F0914(41CL3GX}6ToiD`F~ga7J{d`{e28l?F_ z^Vrt%lJXdq#i1n;Lt{4aX@S&?()?}$h?rU;fzIFx7)l|lVuFmA*;e`dv!Ho4zOar& ztyvxt^A5g4>iPfRn|~vCLKq11R2>P^(uJvtX<$jhCkZk4o7|yk6UD~AP~Z1MhXqCy zA5$8)(tasZS~cKQEW|KAmgEB%{FXOv~{kUXHm92I&?b;toR{x_C4q+kwAh5wuB zH^iT|xJi~$;f@0{EazwvgsBPfyEH-a=06ng=E5UQyOKqkv&;ta&BJr zC(2N*TT0V92g0XJ*N(v9b&iCc<8lx_W>&0pDco{VKVXN#93bHB00IGc+H+EZYTmf4UF^x33v(Ocs)pF=7U-v1sT zOhLFixG`|~o$R3z)79-rqzUA9)0~~qPP!db5a~-(i9uH(OawdcDp3w<-7k8Q!(EEi zZpT`*!AK$W2Fea}tB`MmJDwxEn?ImFNSsrg4_B5FoPjoVHo=-*%oWFU#vMv7hm&fG zWBj0KvM1rV!v<~)R0^qtO(l#fvr}bqj;V{LFJ$7vRdb&5HO>W5Jpis*eo_zUM{9P{ zTY}($odKBDb!G8S&&O}CzlVi%j7EvgoX&2D#Fl${WeE&SG&o_yQKQXv};;y8#i1g<`$~)45`Nb3x4k#7~-^DNP_e zU*fn3_~2Qda^jM`O#`=EiD^$!eT1W}POHL*Jw&mJu%>wLCv~l&i|38kX_jA76ro37 zC)+$6bza(U5xT{9xdcf@T&XP@fR+ZJwc$N0AJ}~^N3@mvWgyI%tEA za}GAxBF(`UaA@CN_w6-2>72?m>Hpil`-{SvJhWj{+!TCD?0HUKC;S$`+kkG}KQ{7)GU9w76Onb_^*^C{LxTwsF39=&g&Lk@ zzWevP9;8?dXt z!tjQXi}?Xn2f%-ZZNq&A3d3#>S`PrbP6GkY@z)UQp?N&X%95sIV2ZpjFeGrL`>k#- zt%tb1xzgf%_8@lU<=xG@RD|pGoTFx%DZAcii*lVf-iK6-$IY8{7LV4z!xOG-gd@F^ zUXrki@lZEVarirkNYWSzbT}!kg>o!9^~_j+4O6tvqTGM5buSm1#ZON()!nEG~J&O zE4YzD4@SZ~8bQXCFF)Z*CJ=19Y%#ELgHNy<(+2kZ^;hS1jfg|mW8xGNg}=tDG=PIu zOk!6y0U?=&Vnm+&xv#oCseum<&qO7ty$CcHP4r9X5H{%djJpSnrkYth^ap(cj2x%= z4x!-V;`@I|%(oP0qn{|%mC>qtuxjfoXR-wc^uVim<*%dt!=pMw)L|RB$U=fEo$0%G zOm`=ppCzWq{cFrHvileOI5Xi!qiPm=Q>w{{w`U)S&g4zYKl>9PESn%1G@@DM$?pYbEa|k5 zfxOSW%99~4X2v?SkHPq?2!DPNI!CsnqK5ZZ?7{MgPlh)Q?K&6mz%!#s50sdJZIH%= z0i8n)_0o(XBfn&?Q+U8~(V}-aUC6tbu?PR|Sv*(qK=oji4aDzp{Fcw2c#Ue(DdovR zLW{RFA42ES%^r~(Vr$XJp84|&YLbE3GN2(Xhx~T~`!CCLwHt5G;I^bqy4;~lbY+vL z2%1KiU%roR_FNQWo;f^hw8l0HajVn_M7izqMY`d%z4M zJ7JThc%U6m*fa!__*Xj{!K>fpQv$1YC{wp!rSpJUs!zjVN_yIW#n*OA;e&slp1|x3 zPb%?AQ&6>N9j&}`s%tYVDvNZ;sP?$1rm%Iq^Hx+p3Ia}oh2bK#0U-uml;bEi!PHN5 zAq(Ub-jn)MDhu9m&T%pOsT8}^v%T@PkO{N#jv-D@ONRlE@rSUPS57`ujp(~X;yu%9 zGK1zEImPnpp~T}Iy5S| z#gz`ccZ2t3r{_A;-SFqSfZ@8o*Em1`jL_%z7r(S?(jQt6;1s|wl`(vGL{Y>b>ahIQ z6?on~K3wwJ@Wef6T6E>m)jiwtw6a0?8$=zZT)3J&UFTA7u56YM!di@(!1W=wU9cO0 z6`EX>x+t8!&b?pR8?mFaiL=Aq=Nz;_4jU6nBc?Z&wWJ)O&!vrUP?_koBpLP}qwX@o zYFq?pH`2H(Cm+D0^eu=2{jUtMXd*7og>1f7KH*2mhqiBw$$oirXeIv&J-ufz#gb@n zZ?FJ!#p%k7m1(#$gRL``bffDJ=a1hxl+T+b1gK-s7^vhU0vJGo| zs52j3_zW28+?BX7a*lq=T5#Jpq5pvocMIx)KX$`c6ywaKo0K}VI>e^PONN-gkx3yj zVf>J+1yCS*VWf*=#Jml?vBvJ>T&oOwWW5?mQ zuP`8*Py&501AVGHZ@ULz#xLOZRCeQKhYI%Y$^j@AvC?O@wR>&!;R4f>YTFWNjG=;8 z$P#}&*=L=03a%OS=;6HNiKYlA3hJxEkz1IdgQxO^s{-8KJkSg}e{{A+?!S5$o23tp z_63nsA3p-T=|mSF>|G2oxl=~FLW&18@&_CawLEgMy!?O<68wr81qv)KMY$MTcnZ#W z31IwdX24X98$RMs3-K^CUn#C3i8=~IVHER-Vfm4Ia^k^W$Izll1vODqp2%YT@@KGi zcD3vKj%TW*Jv+)@UNTWm37Fer1J6+7a%iB?81k>mvP*L(*j5{b?X=)kFbCmi1H)tJKQ&RAA7BF%(0RA}FZWBHRfPCIXX0AW)u4-IyGix`K(wnFCl9w{-%N1ZW;^l0yp%p4 zW^qh&+iq-&?H5?EG@2*55T(ZebY3*S>j26!QldJEW*%kb=V8arGckqyP&h$)*Vxmu<$uH4TjTG2cp4PI#n1-5Y*foAU zbE$Eov=0`>d}6+p?FmbFrng{yMe_1y{^3P;j+uOPEbCB+?HQ@8(;$D|#NxLkwDzZu zB3De3Z61zxc8YeEMl|=H9k7rVj7d`pGqc5-Ov2As_!?rVhOsDbWRv7aT_rU*XAnV! zSu1N0QNj3goHB0Yf01rCen}P02J0|>qfj92O^1=dg_rCcrb)Pu%oz97|9DV5iIpQe zFlM-QGK7CSTja!c8>EwLTT}`C@M|%gY|;F3w!uqsQ_PN5oCk<^xMSPQua9>6&c0^lKP*c|1D$j9x6x*jMug?(_2I4}Nh(I)Rg6MZKF|xgPz$LcMj zPJ!L{<{&HLi2;atvWt1ji;bGeH79+y|2ekGSI-P!BAB^q&)3<8oSD3sKX)S^g6;nk z(GR1u&f!&F&%JaDLVjBUKF=o#$R~29OYv{L@81deT?%g#zy>;{R3FN9V91mmaS&vy zm!^lE{O@fC=gNqtHjOyEQ1+j@Vc?6mhZTnxM(uK902K8JHoUr@8Ob-DlVi`Gl^UEh z`wxj;A(m`gf7HD&T@1?3xrW05T%SJWNr8MGy48Qe`L9erglJEdM`RK-@3J=;clUiy zWBuO^g1kI)q3ul{k)?F}dSxg%O3`r@oB&Ea#qkE(4lwgjU(Beu#Mzle~LrZ_QVXE(ZT3Qmp6940yj01*ibC z@qU_8qL-onPD{asRf)A8oq3-rGc@v(Kv=ZX*ztSQ%M`N3;J=a%)B@4N(&n{sq5Pt` z-t9DkoMr>xN66%$qDsbv)&u=6yWwDNfvdTcHyp*W56tl+;$IP6`+;2=J{#ESFK~g0 zb8BO4JQh?hgxcUH*bK_J(WYK%#v+pNQga@;No6h3hwC-XQeDdab0t`{0khiEK?=O< zjErMHQ*d-~6hb38bbQn*=A+3|$`HAbkLv&JMUvf22_AfiHojo+2+c4q{_$au7fpb95AF3vh#fRDRs=;TQf=%)TVnQ?0bSQmO-5}K|>arJU*}9f$v~h^h zA-akg!l;DZ&4jgeZGcnG{Dn8h2JDWp?T$4vMl@jjwj5{RN?kKE9FTMH;iJ7xgq=}^ zV`%1OB+~pE&A?AOdNYPd>dly&VBpQjBG!j3F5HezRSxcYNR_~% zO|*E8n>s5b@S$t=FV0?U5GL2w)j42(sCme#Z=$4xdp7m}J2?g$)6~fr1gw<5O=II~ zo0$l}PU1saIkbW`N%?GIrqZQ9z&KD}wf2T<>NgC=`DOy>s=TFml6VLx zB8|@xj1{~3mcEj_oY(katc~a=pz_ZOZACrac;eD;gF)TqfK<2OM@GFwE8Yln=$(S| z#?}jDw``%kpg@B&i3N&0d-57k>p>U!5+@@5bEYXk?X+8p9?ywi$F+VXU`q{i?T3X_%99)H~8y z-$@+4Tz(%TG++h4Oyp}**eLc(Fo8GdqK>3@cGuk+Y{Fkx|=}LrwN6cf3PORxJs;I=I0qy8SWDM22lpO{rQjtlsE140p= z08+7>vt7}%^Rw-JA9!c=Uo>crB@hw!M*0C_=K-8a^bqEK!B2#r^(Rc7y6~(at#!b)LT@5QJT(~{-PdQ zSU+WRw7uK8_BH~q{xKBw82MtwmHrr6^<5yD|2SiVH-5o*#=YQ$`S^eZIr)=+v|a%v zKw(k(_Cv9KYvrgxD-UEvp&H+KOR1jm!BVKe5&5ROL3vw7q(BaAjTzirPA|vs@R6YJ z;Sb^WfFNO9T6hWk)BS+t7e5E;C|CS(hn8D5<0s@w3r;?j4y!MOxLZFup^g ze=o%7m=8gFjV*DkcM5~*H@Mp2nJL`T?J%%iu6uKmff>P*0igmvc&GfJcev&qhC_sv zI=?Ajy6t*{L*pa+`$TW;yjbQy?%356muo>Be3%%M7=&24YEEETJEJ7yc`7~Ds&r;p z3ja4=udY>5nTXz_nGlh`UtucU*MI;Eq0FjI=c?!QdWfYQzk9~j>$zUg=lt=HF$GN^ zV?(3)7NeMKT(=Uoojw?>@(YE=sfwZ|P~>Nswevx58lPd-H5Cw*7Zw3yO&{b2Dfjj>q$$bN8GTYtuJIOye@({BY;97^#EQZpPBD<{ z^E`#imgq|nHMA8NcoS+MoibSZ=h;taZ4BR4@Z?MAc$+SV3tpvw7l->G(%xm{@{g%AmMT9aT3Gl3rsvI6G7=Law8u& zI3fRgGVg0!e@hblo`qDz%2Dbmf{$ywggW!FkN{mq@pndqFWdG<0yG3MFs=11jJg^} zWk&phm!$bP$WfnAhnRW93PNQ2?c74>i2=!2i9LCD)GYV?(t6%z&?$AeW`!zt8>c7J zTbTpKcTH9E{?B2=vf}77L;eq6s)KUve$t1pxFtrx2EEu;mCNFx9e8P#++y9n(Wk?R zY*tj3xS?QwpZuJD8Vy%d0*Q|n$<#DiM;8`NI?on8#liy33`|O(3))zUL=26;HBcpR zkyG1siGliPbDbS_cA}!q+j=UQ>A2~C(b6wqC*vHOeyQ{2PZkgLolBVzOPM@UD&tLe z#RX>k2D2=shCYIWJd%YxsyLua{*qqA_r=QB%*05{#7N4-NVY)l7U<0ZXJ6>CSK;Gp zfbvBM^dSiJDO;yy?}RXk`}fGez)pt4PUdwYrK4N+D%$3{pFc{J`24Zvv46+febHZ* zyDM%)9%W@=7it9Y%s$a2~WzWy5FHM)L<*2nBM$ty0E(o)0Llgm2# z#S#nCMRf|@wDGZ06$MEJ2wj&4o{kkCnt!+tc0?zAhc-kPh`Rx8v(8Ok#(`Q-@=P4( z@*GwRw|yTqb=WHvzrOcwCBEW7xtP#@xwnQtdV51`%cL-+3qUDn}oX8`Yd!vL zH@+o=_~p>p2cj3TyR2UE@rX#KFT5bR9b0Y6V|4pWm47RApa(20^8vKB0l6?a1zNTb zIXn`qAMQeF$z0zxy(3$3xSZ8vN6})a0ajtsWT8Y{Y`1G^3S^bVl5<0-|7hoGCU+C5 z#o>tw9Z>G<@oT>7Xxou6RIUFgEQnv9STo@m&DMzmdj#kyq~JUr)rE@Ws1a`Sz?l`3 zYG4iyx15cQzYkD_>zZa$%Og$E^nvmqCpLmN9ywgsjdEP04lnE+7nm);O7>Kg0PD@x zE@&c7x@frjM{FOTwnOyq0bqe2T;Sa(fm`t+z4!58nbMqZp2Hs6lUn&lqM#}H)6&I) zU1_E@SxwvSVoZ57wR#M2$`IFOFpnmFgLYT}{;^OH)Ksqm3YM`rJ#jeZonlIS-d@l% zdyUP;DQfH3v50Q<;XKzN#g{_lrcPKLToNf^RYZ;}XMAx)Z^X9xHp1XV(LG@5J+LqObDhu1BCf{1{ek&qGIsOm>`wb7p)2;Ki zt`%4U6N{7Wt-}7b$Nyp%<%u7qHyXwGQNZ4XxMgpUM2-muae@QEkTA`b3)qG*`@rJ8 z@G8Fwh##J^%^ndJVYx%R|7BP4B812#EV6$jp8kFLY)V+H}m9AAa(;(_kNU?2}ULUmLIR zYuXBbzG%R3d&{f3W#=FDTLHLug;rOzivom7Q*pLt1ipZHrh!DJUl_*=q3tcy{DFM& z7;o{|51Hg%3Pt8V9cbq4JI?7rj_Lh>7?cILLj$4bICRdQ36eRC36c`)oa^q|5w+M6 zJ!^=qD-luD6=@Q4t&N67Ld@`gwBxj?3y`rgR0bOREy|;D*MegJapp6~Ze4iHCJo#H zzCq`%I4fIFhDRVh_IF-y!@iCIMMeaz213O;Gb|U(d%e=rX1xWw=4k0g2V!0n!-E-g zi)3eJQG&tUr2S0!jbn6z5djA{9U(I{)c0#BPIX7SBkSRrCH;v_C;!cbsZ1=YSE)?N zrX>RD3hqWTm)9jgbU);&#NUv`Bah!sT4aM|3e5WI#M}0gq6&*dR!?7j?~aH~Ny^7# z_TkU;>r##__sLN~U!6^@Z}VBb&Hj(v+CuyMs;cs@$3%~rpZPwIc1c{TakVVVcAGsE z_JbG&YR!b`pa2RlzT%4Q=T@?@Dd!qofW`jp$*e0LSC|Ok3XqCfFCiHITj)+Pk{~T= zMY*qsv-uID)n1S*appkpg)V95DPpdjHj9jaC0^Cj!kY!|RexHaIQ8ABKBGQkD#)Q4-LwAdGj$fy-xUL% zNyqQ|_QctM8N!ZX3#w9+-ZO^L=Qwu1wLM(6xrdOj{+{_cwK1PQHH|i*Hz)3!Ms`^) z;==u=nm^9>EZe}1Mt>M*&K9Ix4bN+2Z<*EpEY*FlRtLzuErBpECE3U(AH=Kd>imBd zS$KG0zqLsp;|eg6Qd2#MdH84;eY;PgGzj7vvU4_pnyiCy4VlLp1-^f+xnYq>B6{Us z1wF`~#>r7;95KiQB%?<2BQYo__#{LL4`y|u$+WL7ORlR$-<;{?OL8|cv`$ORY`|*N z5JJwT97iJkAUhg~RQfy%@%=HO8%9hh*Gn`lwQ-9ln(2QgXq_Zzgic(`g8`|IlG()6 zzIj6cY~%;8BWBVC)OfCF^XEAhyVSQ?^5)^l%`k&`yv;!c3ikG?XD5Ss4^16mLlkPh zYS|3d50*uzrDsB%Iu^rjzl)LyvC%CK~hEH=^;jFx9mO z@4xK7z(thpCpx}HBfEhIi9O~fj4X1%?R9c|-T>Ep6P{M(W$;~*CkbUK;A$#kjG6Xv zF>Moqc?zS7LCLy?E_buXo6tW&nsqFSJQFTv^u^!Xt1as3yJ{CK^{o*o{3$TBi)3T^ z@1S89X3I5L?PEx4hv2SM*q~_J`8G?>eJjqHg1Q})*=B6*~u`Nk)zEp9woA0f)(Ml_L&wRp~o#=WfQM zd@eT=g9ZY$owxwZbB$Aq=k9yoJMJy1_rfx_6+xsr-%vEhYl&|?> z97o=OZ_)$nYjL5a_9$e|kno=&S9t@+pXn_wZbg@1F*191&_i|@?z{Rba}LAp9R$AD zQi9sLtb1*&>QB!^yjz0b@!$x5NK;EYeUYFZZoYg5-pd(UifX-g}8 zP9K=HeBVqs0DS5@Yg6pc(nmE$nSx-|eArsHRW)Nu@t%A9i^ym_s{BMXQPc}8NJaAFArbO{&H z&9J|DX>eFo6crU++>=IQoCuF77r}mj?FuJ9E1~d}7UdATGwRm%%^fQ>Czj;GvVM+Y zk!c+(Riv!)NStEx8FibNGR!Jzg&-HD?0);eA$_xtG5OYe@L_t&$Ep@Z@z|=+=cJq3 zrSCt%XPI?00rA4tJfQ*V~p9Qr|neegE+;o#@j zGP83EZzw3{?Bopdzdte9w9p>c_dy`3nJBhZrQ=)Wuk09vSQrSYF$udjBaYTbc;c}e zFc(EroJ!ywd40(`$(faPZ}d|CPNSpzThfs}3KfF^D}4#CL&p|Y=atGHfRAYBr~F;| zWnP3~Pqd2_Euxcbs2$dYSS%aJKowpf8}b?n_f{uIeUgm$OE&Zi)@AHa)c$HT4(>+F zmq_BwO}d40_IUcAMVJ$_jg#@F^dR!Fo#IKgjP4`)>$bIpA;IdA<~geEnf`uaCPqS1 zGU-C*`P#W{#Z>O)G_DIhu;CtkDb^|33gO?3^9u7YrqJPE_5-7Z=6BCRX5fytSO+J8 zYPlF@Wx;Hw8{7rIdF~u$lRu4*MELc6?z+sHaLP)qz8% z+;v)iixW)i8K#^+%xsY>Zt}#6kjX@FW{u#ix{>J{ap@aT=^J6`Ky`U_H1JHV=@G$ z>*%)3(01pZ(c4YkpY*wt-RYygAN~@xSeBQl;|A-(t`q=O4YjZ;>UM-V_{+wZRpA;Q z9oKZiFFXp)wdSL+5Z{Km-*zgH8siFLZ!syZ+L0^ZdGb8|%8|v6H{gWU(ECkUOjX6mt?1wMS<1DbKPot zljRqoi}Cz^rFs63h3aKP{rNLt@2LX~r?1`IvB9M5gFl}ReNXjCzcjj!`PyA2`*^u; z>XVNmaz%@l#iM)rszh;-^?EAfs+T|IR~*`0S|9p;w)*sUt^YMHHdD*%=Z=I~F6UgC z{`Fb4Rm!Jo{B zEb8+wUDfTcUDdE;r}Me(-swjj$%2N#uLRxu%w|dya*IDa9Oqujy^xZT`1`Ea=Xz$# z+CzCxldI1Cu&UvNy8b+0KE8ST4fA>9w!?qC`kAb}%OG;s0_S;ZVmX)Zka(*S%(XYV z$U~P>62swWR${p|zc%4ky|Z3b+Yi6%S<;ipW<}e!99(E>l>c z6=fX0)zqTUIqJ09Yuvnr>t4q@kuCf>ynOq>2gMfk=Lb%-y``Q$J@`hLzwYCR>~PUG z)tv|Fux`e~)oA+hV3tZ>tP>GhCVZr-(lM`P#5 zrVpJy_NDQ4%dxUQIwFt%-7iB_wm*iKTCHW?A=~VF7WwD|N2*i!uX1wE?BO zu+sWmpe>d19C0NakLs3FM%9(ib zaVRrr0D!(eJ=QJM`R_LxVv0-$0HuZjKz)%DYH@|JL?brhv!%+pbO#UgW(|jn-f)AC z1Ug7Vb>js@jc8bnM2$`(0BQ!$?gdBUcT>=dWzE6blN*4wnp6A%1`#3|O1sc+E zXP{TcdL9I-aL3RkF9yPXg{wV&y&ElCL#^FdN;EXa<`7|0lnxc2^go8j^#FS(W@wdWE1A+94;Vuff9`=5mqch9tSoJeE_gy>U5KJR zaZ%Un+C|KcuhCFqiZ*0}LcJg(<%v9PeE|=E)?ENZX-Vnb!H_O_L3bi$!V9K|@}wzI!wzuVEm3`j+Irk*2SCW5~agfeMx}E3=bh zzS6aq#s@Dli{@N6M=e6Ak0d_On$T4~7;gMRv#2||=)>NSZ)nJ*n}P5RF+=aJg@!1= z7YYb9*%#yOy$p_RAgxzK_0!PSVFpqUb9{*&2l}>mSiR_`r=@LPBUOZgU+v$@tZC&+ zqzGs!>a7V+H{uH?@x$@WY6`W^a}+BF6BvNg(UEL* zb|m$GC(}S<3vq-JiHz1l;>w96YjN5$o}`uZNX2A<3v_xoiS2~%j1{t#v6EKHFm`1N zg&sJ<^%|E;E3ye%_pA4fNV5t0edC)#p=TMCJ2H{8C+Z-_efBeHG0KobD{>^qPr6u7 z;2yQQ07Ph7&?B8ewQ?9wieO`Vml)4ofae~hRkR*)1KEddTOeMbH;1aOa}}c6KA+mAs%C6H|bI) 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 142/148] 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 143/148] 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 144/148] 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 145/148] 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 146/148] 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 147/148] 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 148/148] 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));