From aacd53d3d417b610745ed35601401dbc7d88ca3f Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Mon, 11 Mar 2024 11:45:42 +0530 Subject: [PATCH] Merge 8.0 (#103) * fix: remove db password from logs (#89) * fix: remove db password from logs * fix: mask db password * fix: Add tests * fix: Add more tests * fix: PR changes * fix: tests for connection pool (#90) * adding dev-v5.0.6 tag to this commit to ensure building * fix: cicd tests (#92) * fix: logging test (#93) * adding dev-v5.0.6 tag to this commit to ensure building * fix: flaky test (#94) * fix: make connection pool test less flaky * fix: test * fix: typo * adding dev-v5.0.6 tag to this commit to ensure building * fix: adds idle timeout and minimum idle configs (#95) * fix: idle connection configs * test: protected configs * adding dev-v5.0.6 tag to this commit to ensure building * fix: cicd (#96) * fix: cicd * fix: test * adding dev-v5.0.6 tag to this commit to ensure building * fix: typo (#97) * adding dev-v5.0.6 tag to this commit to ensure building * fixes tests * adding dev-v5.0.6 tag to this commit to ensure building * fix: Vulnerability fix (#98) * fix: updated dependencies * chore: version and changelog * fix: versions * adding dev-v5.0.7 tag to this commit to ensure building * fix: dependencies (#101) * fix: dependency fix * fix: dep fix * adding dev-v5.0.7 tag to this commit to ensure building * fix: fixes storage handling for non-auth recipes (#102) * fix: non-auth fix * fix: migration script and test fixes * fix: plugin interface version * fix: remove unnecessary func * adding dev-v6.0.0 tag to this commit to ensure building --------- Co-authored-by: Ankit Tiwari Co-authored-by: rishabhpoddar --- CHANGELOG.md | 25 ++ build.gradle | 16 +- config.yaml | 11 +- devConfig.yaml | 11 +- implementationDependencies.json | 6 +- ...lugin-5.0.5.jar => mysql-plugin-6.0.0.jar} | Bin 206487 -> 208449 bytes pluginInterfaceSupported.json | 2 +- .../storage/mysql/ConnectionPool.java | 6 +- .../io/supertokens/storage/mysql/Start.java | 34 +- .../storage/mysql/config/MySQLConfig.java | 43 +- .../storage/mysql/output/CustomLayout.java | 4 +- .../storage/mysql/output/Logging.java | 11 +- .../mysql/queries/UserRolesQueries.java | 12 +- .../storage/mysql/utils/Utils.java | 17 + .../mysql/test/DbConnectionPoolTest.java | 396 ++++++++++++++++++ .../storage/mysql/test/LoggingTest.java | 276 ++++++++++++ .../mysql/test/SuperTokensSaaSSecretTest.java | 243 +++++++++++ .../test/multitenancy/StorageLayerTest.java | 6 +- .../TestUserPoolIdChangeBehaviour.java | 27 +- 19 files changed, 1098 insertions(+), 48 deletions(-) rename jar/{mysql-plugin-5.0.5.jar => mysql-plugin-6.0.0.jar} (71%) create mode 100644 src/test/java/io/supertokens/storage/mysql/test/DbConnectionPoolTest.java create mode 100644 src/test/java/io/supertokens/storage/mysql/test/SuperTokensSaaSSecretTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index e91e117..f5bbbaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,31 @@ 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 FOREIGN KEY user_roles_ibfk_1; +ALTER TABLE user_roles DROP FOREIGN KEY user_roles_ibfk_2; +ALTER TABLE user_roles + ADD FOREIGN KEY (app_id, tenant_id) + REFERENCES tenants (app_id, tenant_id) ON DELETE CASCADE; +``` + +## [5.0.7] - 2024-02-19 + +- Fixes vulnerabilities in dependencies + +## [5.0.6] - 2024-01-25 + +- Fixes the issue where passwords were inadvertently logged in the logs. +- Adds tests to check connection pool behaviour. +- Adds `mysql_idle_connection_timeout` and `mysql_minimum_idle_connections` configs to control active connections to the database. + ## [5.0.5] - 2023-12-06 - Validates db config types in `canBeUsed` function diff --git a/build.gradle b/build.gradle index 85e59dd..12b2dca 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'java-library' } -version = "5.0.5" +version = "6.0.0" repositories { mavenCentral() @@ -20,7 +20,7 @@ dependencies { implementation group: 'org.mariadb.jdbc', name: 'mariadb-java-client', version: '2.6.0' // 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.gson/gson compileOnly group: 'com.google.code.gson', name: 'gson', version: '2.3.1' @@ -32,10 +32,10 @@ dependencies { compileOnly group: 'org.jetbrains', name: 'annotations', version: '13.0' // 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' testImplementation 'junit:junit:4.12' @@ -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 39df059..30a9555 100644 --- a/config.yaml +++ b/config.yaml @@ -75,4 +75,13 @@ mysql_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. -# mysql_thirdparty_users_table_name \ No newline at end of file +# mysql_thirdparty_users_table_name + + +# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: 60000) long value. Timeout in milliseconds for the idle connections +# to be closed. +# mysql_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. +# mysql_minimum_idle_connections: diff --git a/devConfig.yaml b/devConfig.yaml index b24223d..94c7164 100644 --- a/devConfig.yaml +++ b/devConfig.yaml @@ -71,4 +71,13 @@ mysql_password: "root" # (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "thirdparty_users") string value. Specify the name of the table that will # store the thirdparty recipe users. -# mysql_thirdparty_users_table_name \ No newline at end of file +# mysql_thirdparty_users_table_name + + +# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: 60000) long value. Timeout in milliseconds for the idle connections +# to be closed. +# mysql_idle_connection_timeout: + +# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: 1) 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. +# mysql_minimum_idle_connections: diff --git a/implementationDependencies.json b/implementationDependencies.json index b814823..f6add0f 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" } ] } diff --git a/jar/mysql-plugin-5.0.5.jar b/jar/mysql-plugin-6.0.0.jar similarity index 71% rename from jar/mysql-plugin-5.0.5.jar rename to jar/mysql-plugin-6.0.0.jar index 574146d660b42ab44bcb52dd8671840fcee26d10..7364546d81ddfd1a3aeea4489a7687789bfaf4cf 100644 GIT binary patch delta 52677 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 Y5lz+ckBi19PCevB)79F3J)U0jKlh{Ai~s-t delta 50680 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 diff --git a/pluginInterfaceSupported.json b/pluginInterfaceSupported.json index a5fdc62..e9d4c14 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/mysql/ConnectionPool.java b/src/main/java/io/supertokens/storage/mysql/ConnectionPool.java index 6f0e26f..7f496a5 100644 --- a/src/main/java/io/supertokens/storage/mysql/ConnectionPool.java +++ b/src/main/java/io/supertokens/storage/mysql/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"); @@ -129,7 +133,7 @@ static boolean isAlreadyInitialised(Start start) { } static void initPool(Start start, boolean shouldWait) throws DbInitException, SQLException { - if (isAlreadyInitialised(start)) { + if (isAlreadyInitialised(start)) { return; } Logging.info(start, "Setting up MySQL connection pool.", true); diff --git a/src/main/java/io/supertokens/storage/mysql/Start.java b/src/main/java/io/supertokens/storage/mysql/Start.java index ddf2527..a4dd0e3 100644 --- a/src/main/java/io/supertokens/storage/mysql/Start.java +++ b/src/main/java/io/supertokens/storage/mysql/Start.java @@ -102,6 +102,10 @@ import java.util.List; import java.util.Set; +import static io.supertokens.storage.mysql.QueryExecutorTemplate.execute; +import static io.supertokens.storage.mysql.QueryExecutorTemplate.update; + + public class Start implements SessionSQLStorage, EmailPasswordSQLStorage, EmailVerificationSQLStorage, ThirdPartySQLStorage, JWTRecipeSQLStorage, PasswordlessSQLStorage, UserMetadataSQLStorage, UserRolesSQLStorage, UserIdMappingStorage, @@ -112,7 +116,7 @@ public class Start // SaaS. If the core is not running in SuperTokens SaaS, this array has no effect. private static final String[] PROTECTED_DB_CONFIG = new String[]{"mysql_connection_pool_size", "mysql_connection_uri", "mysql_host", "mysql_port", "mysql_user", "mysql_password", - "mysql_database_name"}; + "mysql_database_name", "mysql_idle_connection_timeout", "mysql_minimum_idle_connections"}; private static final Object appenderLock = new Object(); public static boolean silent = false; @@ -1851,7 +1855,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); @@ -1860,9 +1864,6 @@ public void addRoleToUser(TenantIdentifier tenantIdentifier, String userId, Stri MySQLConfig config = Config.getConfig(this); String serverErrorMessage = e.getMessage(); - if (isForeignKeyConstraintError(serverErrorMessage, config.getUserRolesTable(), "role")) { - throw new UnknownRoleException(); - } if (isPrimaryKeyError(serverErrorMessage, config.getUserRolesTable())) { throw new DuplicateUserRoleMappingException(); } @@ -1933,6 +1934,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 { @@ -2966,4 +2977,17 @@ public static void setEnableForDeadlockTesting(boolean value) { assert(isTesting); enableForDeadlockTesting = value; } + + @TestOnly + public int getDbActivityCount(String dbname) throws SQLException, StorageQueryException { + String QUERY = "SELECT COUNT(*) as c FROM information_schema.processlist WHERE DB = ?;"; + 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/mysql/config/MySQLConfig.java b/src/main/java/io/supertokens/storage/mysql/config/MySQLConfig.java index 4ac4af3..52bc3fe 100644 --- a/src/main/java/io/supertokens/storage/mysql/config/MySQLConfig.java +++ b/src/main/java/io/supertokens/storage/mysql/config/MySQLConfig.java @@ -108,6 +108,14 @@ public class MySQLConfig { @ConnectionPoolProperty private String mysql_connection_scheme = "mysql"; + @JsonProperty + @ConnectionPoolProperty + private long mysql_idle_connection_timeout = 60000; + + @JsonProperty + @ConnectionPoolProperty + private Integer mysql_minimum_idle_connections = null; + @IgnoreForAnnotationCheck boolean isValidAndNormalised = false; @@ -236,6 +244,14 @@ public String getThirdPartyUsersTable() { return mysql_thirdparty_users_table_name; } + public long getIdleConnectionTimeout() { + return mysql_idle_connection_timeout; + } + + public Integer getMinimumIdleConnections() { + return mysql_minimum_idle_connections; + } + public String getThirdPartyUserToTenantTable() { return addPrefixToTableName("thirdparty_user_to_tenant"); } @@ -331,6 +347,19 @@ void validateAndNormalise() throws InvalidConfigException { "'mysql_connection_pool_size' in the config.yaml file must be > 0"); } + if (mysql_minimum_idle_connections != null) { + if (mysql_minimum_idle_connections < 0) { + throw new InvalidConfigException( + "'mysql_minimum_idle_connections' must be a >= 0"); + } + + if (mysql_minimum_idle_connections > mysql_connection_pool_size) { + throw new InvalidConfigException( + "'mysql_minimum_idle_connections' must be less than or equal to " + + "'mysql_connection_pool_size'"); + } + } + // Normalisation if (mysql_connection_uri != null) { { // mysql_connection_attributes @@ -517,10 +546,18 @@ public String getConnectionPoolId() { StringBuilder connectionPoolId = new StringBuilder(); for (Field field : MySQLConfig.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("mysql_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/mysql/output/CustomLayout.java b/src/main/java/io/supertokens/storage/mysql/output/CustomLayout.java index 27f1e4c..74dca68 100644 --- a/src/main/java/io/supertokens/storage/mysql/output/CustomLayout.java +++ b/src/main/java/io/supertokens/storage/mysql/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.mysql.Start; +import io.supertokens.storage.mysql.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/mysql/output/Logging.java b/src/main/java/io/supertokens/storage/mysql/output/Logging.java index 531d76e..084f1a6 100644 --- a/src/main/java/io/supertokens/storage/mysql/output/Logging.java +++ b/src/main/java/io/supertokens/storage/mysql/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.mysql.Info") + ? createLoggerForConsole(start, "io.supertokens.storage.mysql.Info", LOG_LEVEL.INFO) : createLoggerForFile(start, infoLogPath, "io.supertokens.storage.mysql.Info"); this.errorLogger = errorLogPath.equals("null") - ? createLoggerForConsole(start, "io.supertokens.storage.mysql.Error") + ? createLoggerForConsole(start, "io.supertokens.storage.mysql.Error", LOG_LEVEL.ERROR) : createLoggerForFile(start, errorLogPath, "io.supertokens.storage.mysql.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); // We don't need to add appender if it is already added @@ -211,6 +211,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/mysql/queries/UserRolesQueries.java b/src/main/java/io/supertokens/storage/mysql/queries/UserRolesQueries.java index 6208cb3..303bebb 100644 --- a/src/main/java/io/supertokens/storage/mysql/queries/UserRolesQueries.java +++ b/src/main/java/io/supertokens/storage/mysql/queries/UserRolesQueries.java @@ -69,8 +69,6 @@ public static String getQueryToCreateUserRolesTable(Start start) { + "user_id VARCHAR(128) NOT NULL, " + "role VARCHAR(255) NOT NULL," + "PRIMARY KEY (app_id, tenant_id, user_id, role)," - + "FOREIGN KEY (app_id, role)" - + " REFERENCES " + Config.getConfig(start).getRolesTable() + "(app_id, role) ON DELETE CASCADE," + "FOREIGN KEY (app_id, tenant_id)" + " REFERENCES " + Config.getConfig(start).getTenantsTable() + "(app_id, tenant_id) ON DELETE CASCADE" + ")"; @@ -331,4 +329,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 " + Config.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/main/java/io/supertokens/storage/mysql/utils/Utils.java b/src/main/java/io/supertokens/storage/mysql/utils/Utils.java index 9dcb1f9..e68d6dd 100644 --- a/src/main/java/io/supertokens/storage/mysql/utils/Utils.java +++ b/src/main/java/io/supertokens/storage/mysql/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) { @@ -39,4 +41,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/mysql/test/DbConnectionPoolTest.java b/src/test/java/io/supertokens/storage/mysql/test/DbConnectionPoolTest.java new file mode 100644 index 0000000..dfc72fb --- /dev/null +++ b/src/test/java/io/supertokens/storage/mysql/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.mysql.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.Storage; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.*; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.storage.mysql.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("mysql_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 = {"../"}; + + 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("mysql_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(300); + + for (int i = 0; i < 10000; i++) { + int finalI = i; + es.execute(() -> { + try { + TenantIdentifier t1 = new TenantIdentifier(null, null, "t1"); + 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) { + successAfterErrorTime.set(System.currentTimeMillis()); + } + } catch (StorageQueryException e) { + if (e.getMessage().contains("called on closed connection") || 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); + } 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("mysql_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); + + 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("mysql_connection_pool_size", "20"); + Utils.setValueInConfig("mysql_minimum_idle_connections", "10"); + Utils.setValueInConfig("mysql_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), + config + ), false); + + Thread.sleep(1000); // let the new tenant be ready + + assertEquals(10, start.getDbActivityCount("st1")); + + // change connection pool size + config.addProperty("mysql_connection_pool_size", 20); + config.addProperty("mysql_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}); + 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("mysql_connection_pool_size", 300); + config.addProperty("mysql_minimum_idle_connections", 5); + config.addProperty("mysql_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"); + Storage t1Storage = (StorageLayer.getStorage(t1, process.getProcess())); + ThirdParty.signInUp(t1, t1Storage, 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)); + } +} diff --git a/src/test/java/io/supertokens/storage/mysql/test/LoggingTest.java b/src/test/java/io/supertokens/storage/mysql/test/LoggingTest.java index fdc2f95..0f05944 100644 --- a/src/test/java/io/supertokens/storage/mysql/test/LoggingTest.java +++ b/src/test/java/io/supertokens/storage/mysql/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.mysql.Start; +import io.supertokens.storage.mysql.config.MySQLConfig; import io.supertokens.storage.mysql.output.Logging; import io.supertokens.storageLayer.StorageLayer; import org.apache.tomcat.util.http.fileupload.FileUtils; @@ -309,6 +312,279 @@ 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 = "mysql://" + dbUser + ":" + dbPassword + "@localhost:3306/" + dbName; + + Utils.setValueInConfig("mysql_connection_uri", dbConnectionUri); + Utils.commentConfigValue("mysql_user"); + Utils.commentConfigValue("mysql_password"); + 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("mysql_connection_uri"); + Utils.setValueInConfig("mysql_user", dbUser); + Utils.setValueInConfig("mysql_password", dbPassword); + Utils.setValueInConfig("mysql_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, false); + + 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()); + MySQLConfig userConfig = io.supertokens.storage.mysql.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"; + + Utils.setValueInConfig("mysql_user", dbUser); + Utils.setValueInConfig("mysql_password", dbPassword); + Utils.setValueInConfig("mysql_database_name", dbName); + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.INIT_FAILURE)); + + Start start = (Start) StorageLayer.getStorage(process.getProcess()); + MySQLConfig userConfig = io.supertokens.storage.mysql.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()); + MySQLConfig userConfig = io.supertokens.storage.mysql.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 = "mysql://" + dbUser + ":" + dbPassword + "@localhost:3306/" + dbName; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + Start start = (Start) StorageLayer.getStorage(process.getProcess()); + MySQLConfig userConfig = io.supertokens.storage.mysql.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("mysql_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(); diff --git a/src/test/java/io/supertokens/storage/mysql/test/SuperTokensSaaSSecretTest.java b/src/test/java/io/supertokens/storage/mysql/test/SuperTokensSaaSSecretTest.java new file mode 100644 index 0000000..5e052e9 --- /dev/null +++ b/src/test/java/io/supertokens/storage/mysql/test/SuperTokensSaaSSecretTest.java @@ -0,0 +1,243 @@ +/* + * 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.mysql.test; + +import com.google.gson.JsonElement; +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.pluginInterface.STORAGE_TYPE; +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.mysql.test.httpRequest.HttpRequestForTesting; +import io.supertokens.storage.mysql.test.httpRequest.HttpResponseException; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.thirdparty.InvalidProviderConfigException; +import io.supertokens.utils.SemVer; +import org.junit.*; +import org.junit.rules.TestRule; + +import java.io.IOException; + +import static junit.framework.TestCase.assertEquals; +import static org.junit.Assert.*; +import static org.junit.Assert.assertNotNull; + +public class SuperTokensSaaSSecretTest { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + private static final String[] PROTECTED_CORE_CONFIG = new String[]{ + "mysql_connection_pool_size", + "mysql_connection_uri", + "mysql_host", + "mysql_port", + "mysql_user", + "mysql_password", + "mysql_database_name", + "mysql_idle_connection_timeout", + "mysql_minimum_idle_connections", + }; + private static final Object[] PROTECTED_CORE_CONFIG_VALUES = new Object[]{ + 20, // mysql_connection_pool_size + "mysql://root:root@localhost:3306/st10", // mysql_connection_uri + "localhost", // mysql_host + 3306, // mysql_port + "root", // mysql_user + "root", // mysql_password + "st10", // mysql_database_name + 40000, // mysql_idle_connection_timeout + 5, // mysql_minimum_idle_connections + }; + + @Test + public void testThatTenantCannotSetProtectedConfigIfSuperTokensSaaSSecretIsSet() + throws InterruptedException, IOException, InvalidConfigException, TenantOrAppNotFoundException, + InvalidProviderConfigException, StorageQueryException, + FeatureNotEnabledException, CannotModifyBaseConfigException, HttpResponseException { + String[] args = {"../"}; + + String saasSecret = "hg40239oirjgBHD9450=Beew123--hg40239oirjgBHD9450=Beew123--hg40239oirjgBHD9450=Beew123-"; + String apiKey = "hg40239oirjgBHD9450=Beew123--hg40239oiBeew123-"; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + Utils.setValueInConfig("supertokens_saas_secret", saasSecret); + Utils.setValueInConfig("api_keys", apiKey); + 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_CORE_CONFIG.length; i++) { + try { + JsonObject j = new JsonObject(); + j.addProperty(PROTECTED_CORE_CONFIG[i], ""); + 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."); + } + } + + try { + JsonObject coreConfig = new JsonObject(); + StorageLayer.getBaseStorage(process.getProcess()).modifyConfigToAddANewUserPoolForTesting(coreConfig, 1); + + JsonObject requestBody = new JsonObject(); + requestBody.addProperty("appId", "a1"); + requestBody.addProperty("emailPasswordEnabled", true); + requestBody.addProperty("thirdPartyEnabled", true); + requestBody.addProperty("passwordlessEnabled", true); + requestBody.add("coreConfig", coreConfig); + + JsonObject response = HttpRequestForTesting.sendJsonRequest(process.getProcess(), "", + HttpRequestForTesting.getMultitenantUrl(TenantIdentifier.BASE_TENANT, "/recipe/multitenancy/app"), + requestBody, 1000, 2500, null, + SemVer.v3_0.get(), "PUT", apiKey, "multitenancy"); + + Assert.assertEquals("OK", response.getAsJsonPrimitive("status").getAsString()); + fail(); + } catch (HttpResponseException e) { + Assert.assertTrue(e.getMessage().contains("Not allowed to modify DB related configs.")); + } + + JsonObject coreConfig = new JsonObject(); + StorageLayer.getBaseStorage(process.getProcess()).modifyConfigToAddANewUserPoolForTesting(coreConfig, 1); + + JsonObject requestBody = new JsonObject(); + requestBody.addProperty("appId", "a1"); + requestBody.addProperty("emailPasswordEnabled", true); + requestBody.addProperty("thirdPartyEnabled", true); + requestBody.addProperty("passwordlessEnabled", true); + requestBody.add("coreConfig", coreConfig); + + JsonObject response = HttpRequestForTesting.sendJsonRequest(process.getProcess(), "", + HttpRequestForTesting.getMultitenantUrl(TenantIdentifier.BASE_TENANT, "/recipe/multitenancy/app"), + requestBody, 1000, 2500, null, + SemVer.v3_0.get(), "PUT", saasSecret, "multitenancy"); + + Assert.assertEquals("OK", response.getAsJsonPrimitive("status").getAsString()); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testThatTenantCannotGetProtectedConfigIfSuperTokensSaaSSecretIsSet() + throws InterruptedException, IOException, InvalidConfigException, TenantOrAppNotFoundException, + InvalidProviderConfigException, StorageQueryException, + FeatureNotEnabledException, CannotModifyBaseConfigException, BadPermissionException, HttpResponseException { + String[] args = {"../"}; + + String saasSecret = "hg40239oirjgBHD9450=Beew123--hg40239oirjgBHD9450=Beew123--hg40239oirjgBHD9450=Beew123-"; + String apiKey = "hg40239oirjgBHD9450=Beew123--hg40239oiBeew123-"; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + Utils.setValueInConfig("supertokens_saas_secret", saasSecret); + Utils.setValueInConfig("api_keys", apiKey); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.isInMemDb(process.getProcess())) { + return; + } + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + for (int i = 0; i < PROTECTED_CORE_CONFIG.length; i++) { + JsonObject j = new JsonObject(); + if (PROTECTED_CORE_CONFIG_VALUES[i] instanceof String) { + j.addProperty(PROTECTED_CORE_CONFIG[i], (String) PROTECTED_CORE_CONFIG_VALUES[i]); + } else if (PROTECTED_CORE_CONFIG_VALUES[i] instanceof Integer) { + j.addProperty(PROTECTED_CORE_CONFIG[i], (Integer) PROTECTED_CORE_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)); + + { + JsonObject response = HttpRequestForTesting.sendJsonRequest(process.getProcess(), "", + HttpRequestForTesting.getMultitenantUrl(TenantIdentifier.BASE_TENANT, "/recipe/multitenancy/tenant/list"), + null, 1000, 1000, null, + SemVer.v3_0.get(), "GET", apiKey, "multitenancy"); + + Assert.assertEquals("OK", response.getAsJsonPrimitive("status").getAsString()); + + boolean found = false; + for (JsonElement tenant : response.get("tenants").getAsJsonArray()) { + JsonObject tenantObj = tenant.getAsJsonObject(); + + if (tenantObj.get("tenantId").getAsString().equals("t" + i)) { + found = true; + + assertFalse(tenantObj.get("coreConfig").getAsJsonObject().has(PROTECTED_CORE_CONFIG[i])); + } + } + Assert.assertTrue(found); + } + + { + JsonObject response = HttpRequestForTesting.sendJsonRequest(process.getProcess(), "", + HttpRequestForTesting.getMultitenantUrl(TenantIdentifier.BASE_TENANT, "/recipe/multitenancy/tenant/list"), + null, 1000, 1000, null, + SemVer.v3_0.get(), "GET", saasSecret, "multitenancy"); + + Assert.assertEquals("OK", response.getAsJsonPrimitive("status").getAsString()); + + boolean found = false; + for (JsonElement tenant : response.get("tenants").getAsJsonArray()) { + JsonObject tenantObj = tenant.getAsJsonObject(); + + if (tenantObj.get("tenantId").getAsString().equals("t" + i)) { + found = true; + + Assert.assertTrue(tenantObj.get("coreConfig").getAsJsonObject().has(PROTECTED_CORE_CONFIG[i])); + } + } + Assert.assertTrue(found); + } + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + +} diff --git a/src/test/java/io/supertokens/storage/mysql/test/multitenancy/StorageLayerTest.java b/src/test/java/io/supertokens/storage/mysql/test/multitenancy/StorageLayerTest.java index f5f53ce..a01b71b 100644 --- a/src/test/java/io/supertokens/storage/mysql/test/multitenancy/StorageLayerTest.java +++ b/src/test/java/io/supertokens/storage/mysql/test/multitenancy/StorageLayerTest.java @@ -748,7 +748,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) { @@ -758,7 +758,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) { @@ -784,7 +784,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/mysql/test/multitenancy/TestUserPoolIdChangeBehaviour.java b/src/test/java/io/supertokens/storage/mysql/test/multitenancy/TestUserPoolIdChangeBehaviour.java index b0afd4b..77d1ad3 100644 --- a/src/test/java/io/supertokens/storage/mysql/test/multitenancy/TestUserPoolIdChangeBehaviour.java +++ b/src/test/java/io/supertokens/storage/mysql/test/multitenancy/TestUserPoolIdChangeBehaviour.java @@ -24,6 +24,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"); + AuthRecipeUserInfo userInfo = EmailPassword.signUp(tenantIdentifier, + storage, process.getProcess(), "user@example.com", "password"); coreConfig.addProperty("mysql_host", "127.0.0.1"); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( @@ -104,12 +105,12 @@ 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(), "user@example.com", "password"); + AuthRecipeUserInfo user2 = EmailPassword.signIn(tenantIdentifier, storage, process.getProcess(), "user@example.com", "password"); assertEquals(userInfo, user2); } @@ -130,13 +131,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("mysql_host", "127.0.0.1"); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( @@ -154,12 +155,12 @@ 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(), "user@example.com", "password"); + AuthRecipeUserInfo user2 = EmailPassword.signIn(tenantIdentifier, storage, process.getProcess(), "user@example.com", "password"); assertEquals(userInfo, user2); }