diff --git a/.circleci/Dockerfile-ubuntu22 b/.circleci/Dockerfile-ubuntu22 new file mode 100644 index 000000000..1f60f940d --- /dev/null +++ b/.circleci/Dockerfile-ubuntu22 @@ -0,0 +1,57 @@ +FROM ubuntu:22.04 + +RUN apt-get update && apt-get upgrade -y + +RUN apt-get install build-essential -y + +RUN echo "mysql-server mysql-server/root_password password root" | debconf-set-selections + +RUN echo "mysql-server mysql-server/root_password_again password root" | debconf-set-selections + +RUN apt install mysql-server -y + +RUN usermod -d /var/lib/mysql/ mysql + +RUN [ -d /var/run/mysqld ] || mkdir -p /var/run/mysqld + +ADD ./runMySQL.sh /runMySQL.sh + +RUN chmod +x /runMySQL.sh + +RUN apt-get install -y git-core + +RUN apt-get install -y wget + +# Install OpenJDK 12 +RUN wget https://download.java.net/java/GA/jdk12.0.2/e482c34c86bd4bf8b56c0b35558996b9/10/GPL/openjdk-12.0.2_linux-x64_bin.tar.gz + +RUN mkdir /usr/java + +RUN mv openjdk-12.0.2_linux-x64_bin.tar.gz /usr/java + +RUN cd /usr/java && tar -xzvf openjdk-12.0.2_linux-x64_bin.tar.gz + +RUN echo 'JAVA_HOME=/usr/java/jdk-12.0.2' >> /etc/profile +RUN echo 'PATH=$PATH:$HOME/bin:$JAVA_HOME/bin' >> /etc/profile + +RUN apt-get install jq -y + +RUN apt-get install curl -y + +RUN apt-get install unzip -y + +# Install OpenJDK 15.0.1 +RUN wget https://download.java.net/java/GA/jdk15.0.1/51f4f36ad4ef43e39d0dfdbaf6549e32/9/GPL/openjdk-15.0.1_linux-x64_bin.tar.gz + +RUN mv openjdk-15.0.1_linux-x64_bin.tar.gz /usr/java + +RUN cd /usr/java && tar -xzvf openjdk-15.0.1_linux-x64_bin.tar.gz + +RUN echo 'JAVA_HOME=/usr/java/jdk-15.0.1' >> /etc/profile +RUN echo 'PATH=$PATH:$HOME/bin:$JAVA_HOME/bin' >> /etc/profile +RUN echo 'export JAVA_HOME' >> /etc/profile +RUN echo 'export JRE_HOME' >> /etc/profile +RUN echo 'export PATH' >> /etc/profile + +RUN update-alternatives --install "/usr/bin/java" "java" "/usr/java/jdk-12.0.2/bin/java" 1 +RUN update-alternatives --install "/usr/bin/javac" "javac" "/usr/java/jdk-12.0.2/bin/javac" 1 \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md index 36e7a663b..2afb2131f 100644 --- a/.github/ISSUE_TEMPLATE/release.md +++ b/.github/ISSUE_TEMPLATE/release.md @@ -69,6 +69,7 @@ labels: - [ ] SuperTokens Jackson SAML example update - [ ] Supabase docs - [ ] Capacitor template app: https://github.com/RobSchilderr/capacitor-supertokens-nextjs-turborepo + - [ ] T4 App: https://github.com/timothymiller/t4-app ### 📚 Documentation (test site) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8a84e87b..71b6d5c90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,26 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - TODO - copy once postgres / mysql changelog is done +## [7.0.18] - 2024-02-19 + +- Fixes vulnerabilities in dependencies +- Updates telemetry payload +- Fixes Active User tracking to use the right storage + +## [7.0.17] - 2024-02-06 + +- Fixes issue where error logs were printed to StdOut instead of StdErr. +- Adds new config `supertokens_saas_load_only_cud` that makes the core instance load a particular CUD only, irrespective of the CUDs present in the db. +- Fixes connection pool handling when connection pool size changes for a tenant. + +## [7.0.16] - 2023-12-04 + +- Returns 400, instead of 500, for badly typed core config while creating CUD, App or Tenant + +## [7.0.15] - 2023-11-28 + +- Adds test for user pagination from old version + ## [7.0.14] - 2023-11-21 - Updates test user query speed diff --git a/README.md b/README.md index 4ad80fde7..4a96a5bb2 100644 --- a/README.md +++ b/README.md @@ -230,6 +230,7 @@ Melvyn Hills Melvyn Hills
Cléo Rebert

Daniil Borovoy
+
Krzysztof Witkowski
diff --git a/build.gradle b/build.gradle index 3722d4d2d..806add897 100644 --- a/build.gradle +++ b/build.gradle @@ -33,22 +33,22 @@ dependencies { implementation group: 'com.google.code.gson', name: 'gson', version: '2.3.1' // https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml - implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.14.0' + implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.16.1' // https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core - implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.14.0' + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.16.1' // https://mvnrepository.com/artifact/ch.qos.logback/logback-classic - implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.3' + implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.4.14' // https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-core - implementation group: 'org.apache.tomcat.embed', name: 'tomcat-embed-core', version: '10.1.1' + implementation group: 'org.apache.tomcat.embed', name: 'tomcat-embed-core', version: '10.1.18' // https://mvnrepository.com/artifact/com.google.code.findbugs/jsr305 implementation group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2' // https://mvnrepository.com/artifact/org.xerial/sqlite-jdbc - implementation group: 'org.xerial', name: 'sqlite-jdbc', version: '3.30.1' + implementation group: 'org.xerial', name: 'sqlite-jdbc', version: '3.45.1.0' // https://mvnrepository.com/artifact/org.mindrot/jbcrypt implementation group: 'org.mindrot', name: 'jbcrypt', version: '0.4' diff --git a/cli/build.gradle b/cli/build.gradle index 904dc0065..52e2ab2d5 100644 --- a/cli/build.gradle +++ b/cli/build.gradle @@ -19,10 +19,10 @@ dependencies { implementation group: 'com.google.code.gson', name: 'gson', version: '2.3.1' // https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml - implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.10.0' + implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.16.1' // https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core - implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.10.0' + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.16.1' // https://mvnrepository.com/artifact/de.mkammerer/argon2-jvm implementation group: 'de.mkammerer', name: 'argon2-jvm', version: '2.11' diff --git a/cli/implementationDependencies.json b/cli/implementationDependencies.json index 645cacaf4..665c92fff 100644 --- a/cli/implementationDependencies.json +++ b/cli/implementationDependencies.json @@ -7,29 +7,29 @@ "src": "https://repo1.maven.org/maven2/com/google/code/gson/gson/2.3.1/gson-2.3.1-sources.jar" }, { - "jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.10.0/jackson-dataformat-yaml-2.10.0.jar", - "name": "Jackson Dataformat 2.10.0", - "src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.10.0/jackson-dataformat-yaml-2.10.0-sources.jar" + "jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.16.1/jackson-dataformat-yaml-2.16.1.jar", + "name": "Jackson Dataformat 2.16.1", + "src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.16.1/jackson-dataformat-yaml-2.16.1-sources.jar" }, { - "jar": "https://repo1.maven.org/maven2/org/yaml/snakeyaml/1.24/snakeyaml-1.24.jar", - "name": "SnakeYAML 1.24", - "src": "https://repo1.maven.org/maven2/org/yaml/snakeyaml/1.24/snakeyaml-1.24-sources.jar" + "jar": "https://repo1.maven.org/maven2/org/yaml/snakeyaml/2.2/snakeyaml-2.2.jar", + "name": "SnakeYAML 2.2", + "src": "https://repo1.maven.org/maven2/org/yaml/snakeyaml/2.2/snakeyaml-2.2-sources.jar" }, { - "jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.10.0/jackson-core-2.10.0.jar", - "name": "Jackson core 2.10.0", - "src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.10.0/jackson-core-2.10.0-sources.jar" + "jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.16.1/jackson-core-2.16.1.jar", + "name": "Jackson core 2.16.1", + "src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.16.1/jackson-core-2.16.1-sources.jar" }, { - "jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.10.0/jackson-databind-2.10.0.jar", - "name": "Jackson databind 2.10.0", - "src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.10.0/jackson-databind-2.10.0-sources.jar" + "jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.16.1/jackson-databind-2.16.1.jar", + "name": "Jackson databind 2.16.1", + "src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.16.1/jackson-databind-2.16.1-sources.jar" }, { - "jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/2.10.0/jackson-annotations-2.10.0.jar", - "name": "Jackson annotation 2.10.0", - "src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/2.10.0/jackson-annotations-2.10.0-sources.jar" + "jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/2.16.1/jackson-annotations-2.16.1.jar", + "name": "Jackson annotation 2.16.1", + "src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/2.16.1/jackson-annotations-2.16.1-sources.jar" }, { "jar": "https://repo1.maven.org/maven2/de/mkammerer/argon2-jvm/2.11/argon2-jvm-2.11.jar", diff --git a/cli/jar/cli.jar b/cli/jar/cli.jar index a74087247..679236a42 100644 Binary files a/cli/jar/cli.jar and b/cli/jar/cli.jar differ diff --git a/config.yaml b/config.yaml index bc6e7cbce..fdb96d4ba 100644 --- a/config.yaml +++ b/config.yaml @@ -146,3 +146,8 @@ core_config_version: 0 # when CDI version is not specified in the request. When set to null, the core will assume the latest version of the # CDI. # supertokens_max_cdi_version: + + +# (OPTIONAL | Default: null) string value. If specified, the supertokens service will only load the specified CUD even +# if there are more CUDs in the database and block all other CUDs from being used from this instance. +# supertokens_saas_load_only_cud: diff --git a/devConfig.yaml b/devConfig.yaml index 73ccf220d..276b35d42 100644 --- a/devConfig.yaml +++ b/devConfig.yaml @@ -147,3 +147,7 @@ disable_telemetry: true # when CDI version is not specified in the request. When set to null, the core will assume the latest version of the # CDI. # supertokens_max_cdi_version: + +# (OPTIONAL | Default: null) string value. If specified, the supertokens service will only load the specified CUD even +# if there are more CUDs in the database and block all other CUDs from being used from this instance. +# supertokens_saas_load_only_cud: diff --git a/downloader/jar/downloader.jar b/downloader/jar/downloader.jar index 8b94c5ca7..4c8bac4ca 100644 Binary files a/downloader/jar/downloader.jar and b/downloader/jar/downloader.jar differ diff --git a/ee/build.gradle b/ee/build.gradle index 94aeed97c..9e91d7a57 100644 --- a/ee/build.gradle +++ b/ee/build.gradle @@ -35,10 +35,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' @@ -46,10 +46,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' testImplementation group: 'org.jetbrains', name: 'annotations', version: '13.0' } diff --git a/ee/jar/ee.jar b/ee/jar/ee.jar index f12f374dc..1795fb9d7 100644 Binary files a/ee/jar/ee.jar and b/ee/jar/ee.jar differ diff --git a/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java b/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java index 79af8d666..cac2c5cb9 100644 --- a/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java +++ b/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java @@ -200,17 +200,18 @@ private JsonObject getMFAStats() throws StorageQueryException, TenantOrAppNotFou Storage[] storages = StorageLayer.getStoragesForApp(main, this.appIdentifier); int totalUserCountWithMoreThanOneLoginMethod = 0; - int[] maus = new int[30]; + int[] maus = new int[31]; long now = System.currentTimeMillis(); - long today = now - (now % (24 * 60 * 60 * 1000L)); for (Storage storage : storages) { totalUserCountWithMoreThanOneLoginMethod += ((AuthRecipeStorage)storage).getUsersCountWithMoreThanOneLoginMethodOrTOTPEnabled(this.appIdentifier); - for (int i = 0; i < 30; i++) { - long timestamp = today - (i * 24 * 60 * 60 * 1000L); - maus[i] += ((ActiveUsersStorage)storage).countUsersThatHaveMoreThanOneLoginMethodOrTOTPEnabledAndActiveSince(appIdentifier, timestamp); + for (int i = 1; i <= 31; i++) { + long timestamp = now - (i * 24 * 60 * 60 * 1000L); + + // `maus[i-1]` since i starts from 1 + maus[i-1] += ((ActiveUsersStorage)storage).countUsersThatHaveMoreThanOneLoginMethodOrTOTPEnabledAndActiveSince(appIdentifier, timestamp); } } @@ -283,7 +284,7 @@ private JsonObject getAccountLinkingStats() throws StorageQueryException { if (!usesAccountLinking) { result.addProperty("totalUserCountWithMoreThanOneLoginMethod", 0); JsonArray mauArray = new JsonArray(); - for (int i = 0; i < 30; i++) { + for (int i = 0; i < 31; i++) { mauArray.add(new JsonPrimitive(0)); } result.add("mauWithMoreThanOneLoginMethod", mauArray); @@ -291,17 +292,18 @@ private JsonObject getAccountLinkingStats() throws StorageQueryException { } int totalUserCountWithMoreThanOneLoginMethod = 0; - int[] maus = new int[30]; + int[] maus = new int[31]; long now = System.currentTimeMillis(); - long today = now - (now % (24 * 60 * 60 * 1000L)); for (Storage storage : storages) { totalUserCountWithMoreThanOneLoginMethod += ((AuthRecipeStorage)storage).getUsersCountWithMoreThanOneLoginMethod(this.appIdentifier); - for (int i = 0; i < 30; i++) { - long timestamp = today - (i * 24 * 60 * 60 * 1000L); - maus[i] += ((ActiveUsersStorage)storage).countUsersThatHaveMoreThanOneLoginMethodAndActiveSince(appIdentifier, timestamp); + for (int i = 1; i <= 31; i++) { + long timestamp = now - (i * 24 * 60 * 60 * 1000L); + + // `maus[i-1]` because i starts from 1 + maus[i-1] += ((ActiveUsersStorage)storage).countUsersThatHaveMoreThanOneLoginMethodAndActiveSince(appIdentifier, timestamp); } } @@ -312,10 +314,10 @@ private JsonObject getAccountLinkingStats() throws StorageQueryException { private JsonArray getMAUs() throws StorageQueryException, TenantOrAppNotFoundException { JsonArray mauArr = new JsonArray(); - for (int i = 0; i < 30; i++) { - long now = System.currentTimeMillis(); - long today = now - (now % (24 * 60 * 60 * 1000L)); - long timestamp = today - (i * 24 * 60 * 60 * 1000L); + long now = System.currentTimeMillis(); + + for (int i = 1; i <= 31; i++) { + long timestamp = now - (i * 24 * 60 * 60 * 1000L); ActiveUsersStorage activeUsersStorage = (ActiveUsersStorage) StorageLayer.getStorage( this.appIdentifier.getAsPublicTenantIdentifier(), main); int mau = activeUsersStorage.countUsersActiveSince(this.appIdentifier, timestamp); diff --git a/ee/src/test/java/io/supertokens/ee/test/EETest.java b/ee/src/test/java/io/supertokens/ee/test/EETest.java index 3a896b253..7418b24ec 100644 --- a/ee/src/test/java/io/supertokens/ee/test/EETest.java +++ b/ee/src/test/java/io/supertokens/ee/test/EETest.java @@ -1326,7 +1326,7 @@ protected URLConnection openConnection(URL u) { JsonObject paidFeatureUsageStats = j.getAsJsonObject("paidFeatureUsageStats"); JsonArray mauArr = paidFeatureUsageStats.get("maus").getAsJsonArray(); assertEquals(paidFeatureUsageStats.entrySet().size(), 1); - assertEquals(mauArr.size(), 30); + assertEquals(mauArr.size(), 31); assertEquals(mauArr.get(0).getAsInt(), 0); assertEquals(mauArr.get(29).getAsInt(), 0); } diff --git a/ee/src/test/java/io/supertokens/ee/test/api/GetFeatureFlagAPITest.java b/ee/src/test/java/io/supertokens/ee/test/api/GetFeatureFlagAPITest.java index d2932f619..efd133a86 100644 --- a/ee/src/test/java/io/supertokens/ee/test/api/GetFeatureFlagAPITest.java +++ b/ee/src/test/java/io/supertokens/ee/test/api/GetFeatureFlagAPITest.java @@ -54,7 +54,7 @@ public void testRetrievingFeatureFlagInfoWhenNoLicenseKeyIsSet() throws Exceptio if (StorageLayer.getStorage(process.getProcess()).getType() == STORAGE_TYPE.SQL) { JsonArray mauArr = usageStats.get("maus").getAsJsonArray(); assertEquals(1, usageStats.entrySet().size()); - assertEquals(30, mauArr.size()); + assertEquals(31, mauArr.size()); assertEquals(0, mauArr.get(0).getAsInt()); assertEquals(0, mauArr.get(29).getAsInt()); } else { @@ -92,7 +92,7 @@ public void testRetrievingFeatureFlagInfoWhenLicenseKeyIsSet() throws Exception if (StorageLayer.getStorage(process.getProcess()).getType() == STORAGE_TYPE.SQL) { JsonArray mauArr = usageStats.get("maus").getAsJsonArray(); assertEquals(1, usageStats.entrySet().size()); - assertEquals(30, mauArr.size()); + assertEquals(31, mauArr.size()); assertEquals(0, mauArr.get(0).getAsInt()); assertEquals(0, mauArr.get(29).getAsInt()); } else { diff --git a/implementationDependencies.json b/implementationDependencies.json index e0ab94e68..ec4da266a 100644 --- a/implementationDependencies.json +++ b/implementationDependencies.json @@ -7,54 +7,54 @@ "src": "https://repo1.maven.org/maven2/com/google/code/gson/gson/2.3.1/gson-2.3.1-sources.jar" }, { - "jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.14.2/jackson-dataformat-yaml-2.14.2.jar", - "name": "Jackson Dataformat 2.14.2", - "src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.14.2/jackson-dataformat-yaml-2.14.2-sources.jar" + "jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.16.1/jackson-dataformat-yaml-2.16.1.jar", + "name": "Jackson Dataformat 2.16.1", + "src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.16.1/jackson-dataformat-yaml-2.16.1-sources.jar" }, { - "jar": "https://repo1.maven.org/maven2/org/yaml/snakeyaml/1.33/snakeyaml-1.33.jar", - "name": "SnakeYAML 1.33", - "src": "https://repo1.maven.org/maven2/org/yaml/snakeyaml/1.33/snakeyaml-1.33-sources.jar" + "jar": "https://repo1.maven.org/maven2/org/yaml/snakeyaml/2.2/snakeyaml-2.2.jar", + "name": "SnakeYAML 2.2", + "src": "https://repo1.maven.org/maven2/org/yaml/snakeyaml/2.2/snakeyaml-2.2-sources.jar" }, { - "jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.14.2/jackson-core-2.14.2.jar", - "name": "Jackson core 2.14.2", - "src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.14.2/jackson-core-2.14.2-sources.jar" + "jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.16.1/jackson-core-2.16.1.jar", + "name": "Jackson core 2.16.1", + "src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.16.1/jackson-core-2.16.1-sources.jar" }, { - "jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.14.2/jackson-databind-2.14.2.jar", - "name": "Jackson databind 2.14.2", - "src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.14.2/jackson-databind-2.14.2-sources.jar" + "jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.16.1/jackson-databind-2.16.1.jar", + "name": "Jackson databind 2.16.1", + "src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.16.1/jackson-databind-2.16.1-sources.jar" }, { - "jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/2.14.2/jackson-annotations-2.14.2.jar", - "name": "Jackson annotation 2.14.2", - "src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/2.14.2/jackson-annotations-2.14.2-sources.jar" + "jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/2.16.1/jackson-annotations-2.16.1.jar", + "name": "Jackson annotation 2.16.1", + "src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/2.16.1/jackson-annotations-2.16.1-sources.jar" }, { - "jar": "https://repo1.maven.org/maven2/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar", - "name": "Logback classic 1.2.3", - "src": "https://repo1.maven.org/maven2/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3-sources.jar" + "jar": "https://repo1.maven.org/maven2/ch/qos/logback/logback-classic/1.4.14/logback-classic-1.4.14.jar", + "name": "Logback classic 1.4.14", + "src": "https://repo1.maven.org/maven2/ch/qos/logback/logback-classic/1.4.14/logback-classic-1.4.14-sources.jar" }, { - "jar": "https://repo1.maven.org/maven2/ch/qos/logback/logback-core/1.2.3/logback-core-1.2.3.jar", - "name": "Logback core 1.2.3", - "src": "https://repo1.maven.org/maven2/ch/qos/logback/logback-core/1.2.3/logback-core-1.2.3-sources.jar" + "jar": "https://repo1.maven.org/maven2/ch/qos/logback/logback-core/1.4.14/logback-core-1.4.14.jar", + "name": "Logback core 1.4.14", + "src": "https://repo1.maven.org/maven2/ch/qos/logback/logback-core/1.4.14/logback-core-1.4.14-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" }, { - "jar": "https://repo1.maven.org/maven2/org/apache/tomcat/tomcat-annotations-api/10.1.1/tomcat-annotations-api-10.1.1.jar", - "name": "Tomcat annotations API 10.1.1", - "src": "https://repo1.maven.org/maven2/org/apache/tomcat/tomcat-annotations-api/10.1.1/tomcat-annotations-api-10.1.1-sources.jar" + "jar": "https://repo1.maven.org/maven2/org/apache/tomcat/tomcat-annotations-api/10.1.18/tomcat-annotations-api-10.1.18.jar", + "name": "Tomcat annotations API 10.1.18", + "src": "https://repo1.maven.org/maven2/org/apache/tomcat/tomcat-annotations-api/10.1.18/tomcat-annotations-api-10.1.18-sources.jar" }, { - "jar": "https://repo1.maven.org/maven2/org/apache/tomcat/embed/tomcat-embed-core/10.1.1/tomcat-embed-core-10.1.1.jar", + "jar": "https://repo1.maven.org/maven2/org/apache/tomcat/embed/tomcat-embed-core/10.1.18/tomcat-embed-core-10.1.18.jar", "name": "Tomcat embed core API 10.1.1", - "src": "https://repo1.maven.org/maven2/org/apache/tomcat/embed/tomcat-embed-core/10.1.1/tomcat-embed-core-10.1.1-sources.jar" + "src": "https://repo1.maven.org/maven2/org/apache/tomcat/embed/tomcat-embed-core/10.1.18/tomcat-embed-core-10.1.18-sources.jar" }, { "jar": "https://repo1.maven.org/maven2/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar", @@ -67,13 +67,13 @@ "src": "https://repo1.maven.org/maven2/org/jetbrains/annotations/13.0/annotations-13.0-sources.jar" }, { - "jar": "https://repo1.maven.org/maven2/org/xerial/sqlite-jdbc/3.30.1/sqlite-jdbc-3.30.1.jar", - "name": "SQLite JDBC Driver 3.30.1", - "src": "https://repo1.maven.org/maven2/org/xerial/sqlite-jdbc/3.30.1/sqlite-jdbc-3.30.1-sources.jar" + "jar": "https://repo1.maven.org/maven2/org/xerial/sqlite-jdbc/3.45.1.0/sqlite-jdbc-3.45.1.0.jar", + "name": "SQLite JDBC Driver 3.45.1.0", + "src": "https://repo1.maven.org/maven2/org/xerial/sqlite-jdbc/3.45.1.0/sqlite-jdbc-3.45.1.0-sources.jar" }, { "jar": "https://repo1.maven.org/maven2/org/mindrot/jbcrypt/0.4/jbcrypt-0.4.jar", - "name": "SQLite JDBC Driver 3.30.1", + "name": "JBCrypt 0.4", "src": "https://repo1.maven.org/maven2/org/mindrot/jbcrypt/0.4/jbcrypt-0.4-sources.jar" }, { diff --git a/jar/core-7.0.14.jar b/jar/core-7.0.18.jar similarity index 84% rename from jar/core-7.0.14.jar rename to jar/core-7.0.18.jar index ee63b514b..4d0695612 100644 Binary files a/jar/core-7.0.14.jar and b/jar/core-7.0.18.jar differ diff --git a/src/main/java/io/supertokens/config/Config.java b/src/main/java/io/supertokens/config/Config.java index 042af8c9f..a51b8bd1d 100644 --- a/src/main/java/io/supertokens/config/Config.java +++ b/src/main/java/io/supertokens/config/Config.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import com.google.gson.JsonObject; import io.supertokens.Main; import io.supertokens.ProcessState; @@ -31,6 +32,7 @@ import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.utils.ConfigMapper; import org.jetbrains.annotations.TestOnly; import java.io.File; @@ -49,16 +51,17 @@ public class Config extends ResourceDistributor.SingletonResource { private Config(Main main, String configFilePath) throws InvalidConfigException, IOException { this.main = main; final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); - CoreConfig config = mapper.readValue(new File(configFilePath), CoreConfig.class); - config.normalizeAndValidate(main); + Object configObj = mapper.readValue(new File(configFilePath), Object.class); + JsonObject jsonConfig = new GsonBuilder().serializeNulls().create().toJsonTree(configObj).getAsJsonObject(); + CoreConfig config = ConfigMapper.mapConfig(jsonConfig, CoreConfig.class); + config.normalizeAndValidate(main, true); this.core = config; } private Config(Main main, JsonObject jsonConfig) throws IOException, InvalidConfigException { this.main = main; - final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); - CoreConfig config = mapper.readValue(jsonConfig.toString(), CoreConfig.class); - config.normalizeAndValidate(main); + CoreConfig config = ConfigMapper.mapConfig(jsonConfig, CoreConfig.class); + config.normalizeAndValidate(main, false); this.core = config; } @@ -89,7 +92,7 @@ public static JsonObject getBaseConfigAsJsonObject(Main main) throws IOException // omit them from the output json. ObjectMapper yamlReader = new ObjectMapper(new YAMLFactory()); Object obj = yamlReader.readValue(new File(getConfigFilePath(main)), Object.class); - return new Gson().toJsonTree(obj).getAsJsonObject(); + return new GsonBuilder().serializeNulls().create().toJsonTree(obj).getAsJsonObject(); } private static String getConfigFilePath(Main main) { diff --git a/src/main/java/io/supertokens/config/CoreConfig.java b/src/main/java/io/supertokens/config/CoreConfig.java index e02fadb4d..3de06caa7 100644 --- a/src/main/java/io/supertokens/config/CoreConfig.java +++ b/src/main/java/io/supertokens/config/CoreConfig.java @@ -30,7 +30,9 @@ import io.supertokens.pluginInterface.LOG_LEVEL; import io.supertokens.pluginInterface.exceptions.InvalidConfigException; import io.supertokens.utils.SemVer; +import io.supertokens.webserver.Utils; import io.supertokens.webserver.WebserverAPI; +import jakarta.servlet.ServletException; import org.apache.catalina.filters.RemoteAddrFilter; import org.jetbrains.annotations.TestOnly; @@ -197,6 +199,10 @@ public class CoreConfig { @JsonProperty private String supertokens_max_cdi_version = null; + @ConfigYamlOnly + @JsonProperty + private String supertokens_saas_load_only_cud = null; + @IgnoreForAnnotationCheck private Set allowedLogLevels = null; @@ -254,6 +260,10 @@ public String getBasePath() { return base_path; } + public String getSuperTokensLoadOnlyCUD() { + return supertokens_saas_load_only_cud; + } + public enum PASSWORD_HASHING_ALG { ARGON2, BCRYPT, FIREBASE_SCRYPT } @@ -394,7 +404,7 @@ private String getConfigFileLocation(Main main) { : CLIOptions.get(main).getConfigFilePath()).getAbsolutePath(); } - void normalizeAndValidate(Main main) throws InvalidConfigException { + void normalizeAndValidate(Main main, boolean includeConfigFilePath) throws InvalidConfigException { if (isNormalizedAndValid) { return; } @@ -407,8 +417,9 @@ void normalizeAndValidate(Main main) throws InvalidConfigException { } if (access_token_validity < 1 || access_token_validity > 86400000) { throw new InvalidConfigException( - "'access_token_validity' must be between 1 and 86400000 seconds inclusive. The config file can be" - + " found here: " + getConfigFileLocation(main)); + "'access_token_validity' must be between 1 and 86400000 seconds inclusive." + + (includeConfigFilePath ? " The config file can be" + + " found here: " + getConfigFileLocation(main) : "")); } Boolean validityTesting = CoreConfigTestContent.getInstance(main) .getValue(CoreConfigTestContent.VALIDITY_TESTING); @@ -417,16 +428,18 @@ void normalizeAndValidate(Main main) throws InvalidConfigException { if ((refresh_token_validity * 60) <= access_token_validity) { if (!Main.isTesting || validityTesting) { throw new InvalidConfigException( - "'refresh_token_validity' must be strictly greater than 'access_token_validity'. The config " - + "file can be found here: " + getConfigFileLocation(main)); + "'refresh_token_validity' must be strictly greater than 'access_token_validity'." + + (includeConfigFilePath ? " The config file can be" + + " found here: " + getConfigFileLocation(main) : "")); } } if (!Main.isTesting || validityTesting) { // since in testing we make this really small if (access_token_dynamic_signing_key_update_interval < 1) { throw new InvalidConfigException( - "'access_token_dynamic_signing_key_update_interval' must be greater than, equal to 1 hour. The " - + "config file can be found here: " + getConfigFileLocation(main)); + "'access_token_dynamic_signing_key_update_interval' must be greater than, equal to 1 hour." + + (includeConfigFilePath ? " The config file can be" + + " found here: " + getConfigFileLocation(main) : "")); } } @@ -456,8 +469,9 @@ void normalizeAndValidate(Main main) throws InvalidConfigException { if (max_server_pool_size <= 0) { throw new InvalidConfigException( - "'max_server_pool_size' must be >= 1. The config file can be found here: " - + getConfigFileLocation(main)); + "'max_server_pool_size' must be >= 1." + + (includeConfigFilePath ? " The config file can be" + + " found here: " + getConfigFileLocation(main) : "")); } if (api_keys != null) { @@ -663,6 +677,15 @@ void normalizeAndValidate(Main main) throws InvalidConfigException { host = cliHost; } + if (supertokens_saas_load_only_cud != null) { + try { + supertokens_saas_load_only_cud = + Utils.normalizeAndValidateConnectionUriDomain(supertokens_saas_load_only_cud, true); + } catch (ServletException e) { + throw new InvalidConfigException("supertokens_saas_load_only_cud is invalid"); + } + } + access_token_validity = access_token_validity * 1000; access_token_dynamic_signing_key_update_interval = access_token_dynamic_signing_key_update_interval * 3600 * 1000; refresh_token_validity = refresh_token_validity * 60 * 1000; diff --git a/src/main/java/io/supertokens/cronjobs/telemetry/Telemetry.java b/src/main/java/io/supertokens/cronjobs/telemetry/Telemetry.java index 727d2f5ec..215024858 100644 --- a/src/main/java/io/supertokens/cronjobs/telemetry/Telemetry.java +++ b/src/main/java/io/supertokens/cronjobs/telemetry/Telemetry.java @@ -16,20 +16,26 @@ package io.supertokens.cronjobs.telemetry; +import com.google.gson.JsonArray; import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; import io.supertokens.Main; import io.supertokens.ProcessState; +import io.supertokens.authRecipe.AuthRecipe; import io.supertokens.config.Config; import io.supertokens.cronjobs.CronTask; import io.supertokens.cronjobs.CronTaskTest; +import io.supertokens.dashboard.Dashboard; import io.supertokens.httpRequest.HttpRequest; import io.supertokens.httpRequest.HttpRequestMocking; import io.supertokens.pluginInterface.ActiveUsersStorage; import io.supertokens.pluginInterface.KeyValueInfo; import io.supertokens.pluginInterface.STORAGE_TYPE; import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.dashboard.DashboardUser; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.storageLayer.StorageLayer; @@ -90,14 +96,55 @@ protected void doTaskPerApp(AppIdentifier app) throws Exception { json.addProperty("telemetryId", telemetryId.value); json.addProperty("superTokensVersion", coreVersion); + json.addProperty("appId", app.getAppId()); + json.addProperty("connectionUriDomain", app.getConnectionUriDomain()); + if (StorageLayer.getBaseStorage(main).getType() == STORAGE_TYPE.SQL) { - ActiveUsersStorage activeUsersStorage = (ActiveUsersStorage) StorageLayer.getStorage(app.getAsPublicTenantIdentifier(), main); - json.addProperty("mau", activeUsersStorage.countUsersActiveSince(app, System.currentTimeMillis() - 30 * 24 * 3600 * 1000L)); + { // Users count across all tenants + Storage[] storages = StorageLayer.getStoragesForApp(main, app); + AppIdentifierWithStorage appIdentifierWithAllTenantStorages = new AppIdentifierWithStorage( + app.getConnectionUriDomain(), app.getAppId(), + StorageLayer.getStorage(app.getAsPublicTenantIdentifier(), main), storages + ); + + json.addProperty("usersCount", + AuthRecipe.getUsersCountAcrossAllTenants(appIdentifierWithAllTenantStorages, null)); + } + + { // Dashboard user emails + // Dashboard APIs are app specific and are always stored on the public tenant + DashboardUser[] dashboardUsers = Dashboard.getAllDashboardUsers( + app.withStorage(StorageLayer.getStorage(app.getAsPublicTenantIdentifier(), main)), main); + JsonArray dashboardUserEmails = new JsonArray(); + for (DashboardUser user : dashboardUsers) { + dashboardUserEmails.add(new JsonPrimitive(user.email)); + } + + json.add("dashboardUserEmails", dashboardUserEmails); + } + + { // MAUs + // Active users are always tracked on the public tenant, so we use the public tenant's storage + ActiveUsersStorage activeUsersStorage = (ActiveUsersStorage) StorageLayer.getStorage( + app.getAsPublicTenantIdentifier(), main); + + JsonArray mauArr = new JsonArray(); + + long now = System.currentTimeMillis(); + + for (int i = 1; i <= 31; i++) { + long timestamp = now - (i * 24 * 60 * 60 * 1000L); + int mau = activeUsersStorage.countUsersActiveSince(app, timestamp); + mauArr.add(new JsonPrimitive(mau)); + } + + json.add("maus", mauArr); + } } else { - json.addProperty("mau", -1); + json.addProperty("usersCount", -1); + json.add("dashboardUserEmails", new JsonArray()); + json.add("maus", new JsonArray()); } - json.addProperty("appId", app.getAppId()); - json.addProperty("connectionUriDomain", app.getConnectionUriDomain()); String url = "https://api.supertokens.io/0/st/telemetry"; @@ -105,7 +152,7 @@ protected void doTaskPerApp(AppIdentifier app) throws Exception { // wants // to use this) if (!Main.isTesting || HttpRequestMocking.getInstance(main).getMockURL(REQUEST_ID, url) != null) { - HttpRequest.sendJsonPOSTRequest(main, REQUEST_ID, url, json, 10000, 10000, 4); + HttpRequest.sendJsonPOSTRequest(main, REQUEST_ID, url, json, 10000, 10000, 5); ProcessState.getInstance(main).addState(ProcessState.PROCESS_STATE.SENT_TELEMETRY, null); } } diff --git a/src/main/java/io/supertokens/multitenancy/MultitenancyHelper.java b/src/main/java/io/supertokens/multitenancy/MultitenancyHelper.java index 24f1f0e7c..63a5cd7b3 100644 --- a/src/main/java/io/supertokens/multitenancy/MultitenancyHelper.java +++ b/src/main/java/io/supertokens/multitenancy/MultitenancyHelper.java @@ -51,9 +51,20 @@ public class MultitenancyHelper extends ResourceDistributor.SingletonResource { private Main main; private TenantConfig[] tenantConfigs; + // when the core has `supertokens_saas_load_only_cud` set, the tenantConfigs array will be filtered + // based on the config value. However, we need to keep all the list of CUDs from the db to be able + // to check if the CUD is present in the DB or not, while processing the requests. + private final Set dangerous_allCUDsFromDb = new HashSet<>(); + private MultitenancyHelper(Main main) throws StorageQueryException { this.main = main; - this.tenantConfigs = getAllTenantsFromDb(); + TenantConfig[] allTenantsFromDb = getAllTenantsFromDb(); + this.tenantConfigs = this.getFilteredTenantConfigs(allTenantsFromDb); + this.dangerous_allCUDsFromDb.clear(); + + for (TenantConfig config : allTenantsFromDb) { + this.dangerous_allCUDsFromDb.add(config.tenantIdentifier.getConnectionUriDomain()); + } } public static MultitenancyHelper getInstance(Main main) { @@ -109,10 +120,11 @@ public List refreshTenantsInCoreBasedOnChangesInCoreConfigOrIf return main.getResourceDistributor().withResourceDistributorLock(() -> { try { TenantConfig[] tenantsFromDb = getAllTenantsFromDb(); + TenantConfig[] filteredTenantsFromDb = this.getFilteredTenantConfigs(tenantsFromDb); Map normalizedTenantsFromDb = Config.getNormalisedConfigsForAllTenants( - tenantsFromDb, Config.getBaseConfigAsJsonObject(main)); + filteredTenantsFromDb, Config.getBaseConfigAsJsonObject(main)); Map normalizedTenantsFromMemory = Config.getNormalisedConfigsForAllTenants( @@ -130,9 +142,14 @@ public List refreshTenantsInCoreBasedOnChangesInCoreConfigOrIf } } - boolean sameNumberOfTenants = tenantsFromDb.length == this.tenantConfigs.length; + boolean sameNumberOfTenants = + filteredTenantsFromDb.length == this.tenantConfigs.length; - this.tenantConfigs = tenantsFromDb; + this.dangerous_allCUDsFromDb.clear(); + for (TenantConfig tenant : tenantsFromDb) { + this.dangerous_allCUDsFromDb.add(tenant.tenantIdentifier.getConnectionUriDomain()); + } + this.tenantConfigs = filteredTenantsFromDb; if (tenantsThatChanged.size() == 0 && sameNumberOfTenants) { return tenantsThatChanged; } @@ -191,7 +208,7 @@ public void loadStorageLayer() throws IOException, InvalidConfigException { public void loadFeatureFlag(List tenantsThatChanged) { List apps = new ArrayList<>(); Set appsSet = new HashSet<>(); - for (TenantConfig t : tenantConfigs) { + for (TenantConfig t : this.tenantConfigs) { if (appsSet.contains(t.tenantIdentifier.toAppIdentifier())) { continue; } @@ -205,7 +222,7 @@ public void loadSigningKeys(List tenantsThatChanged) throws UnsupportedJWTSigningAlgorithmException { List apps = new ArrayList<>(); Set appsSet = new HashSet<>(); - for (TenantConfig t : tenantConfigs) { + for (TenantConfig t : this.tenantConfigs) { if (appsSet.contains(t.tenantIdentifier.toAppIdentifier())) { continue; } @@ -239,4 +256,21 @@ public TenantConfig[] getAllTenants() { throw new IllegalStateException(e); } } + + private TenantConfig[] getFilteredTenantConfigs(TenantConfig[] inputTenantConfigs) { + String loadOnlyCUD = Config.getBaseConfig(main).getSuperTokensLoadOnlyCUD(); + + if (loadOnlyCUD == null) { + return inputTenantConfigs; + } + + return Arrays.stream(inputTenantConfigs) + .filter(tenantConfig -> tenantConfig.tenantIdentifier.getConnectionUriDomain().equals(loadOnlyCUD) + || tenantConfig.tenantIdentifier.getConnectionUriDomain().equals(TenantIdentifier.DEFAULT_CONNECTION_URI)) + .toArray(TenantConfig[]::new); + } + + public boolean isConnectionUriDomainPresentInDb(String cud) { + return this.dangerous_allCUDsFromDb.contains(cud); + } } diff --git a/src/main/java/io/supertokens/output/Logging.java b/src/main/java/io/supertokens/output/Logging.java index 58106b7ba..d3c89f1fb 100644 --- a/src/main/java/io/supertokens/output/Logging.java +++ b/src/main/java/io/supertokens/output/Logging.java @@ -52,11 +52,11 @@ public class Logging extends ResourceDistributor.SingletonResource { private Logging(Main main) { this.infoLogger = Config.getBaseConfig(main).getInfoLogPath(main).equals("null") - ? createLoggerForConsole(main, "io.supertokens.Info") + ? createLoggerForConsole(main, "io.supertokens.Info", LOG_LEVEL.INFO) : createLoggerForFile(main, Config.getBaseConfig(main).getInfoLogPath(main), "io.supertokens.Info"); this.errorLogger = Config.getBaseConfig(main).getErrorLogPath(main).equals("null") - ? createLoggerForConsole(main, "io.supertokens.Error") + ? createLoggerForConsole(main, "io.supertokens.Error", LOG_LEVEL.ERROR) : createLoggerForFile(main, Config.getBaseConfig(main).getErrorLogPath(main), "io.supertokens.Error"); Storage storage = StorageLayer.getBaseStorage(main); @@ -251,12 +251,13 @@ private Logger createLoggerForFile(Main main, String file, String name) { return logger; } - private Logger createLoggerForConsole(Main main, String name) { + private Logger createLoggerForConsole(Main main, String name, LOG_LEVEL logLevel) { LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory(); LayoutWrappingEncoder ple = new LayoutWrappingEncoder(main.getProcessId(), Version.getVersion(main).getCoreVersion()); 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/storageLayer/StorageLayer.java b/src/main/java/io/supertokens/storageLayer/StorageLayer.java index 3d4c34f83..faeaece0d 100644 --- a/src/main/java/io/supertokens/storageLayer/StorageLayer.java +++ b/src/main/java/io/supertokens/storageLayer/StorageLayer.java @@ -242,7 +242,7 @@ public static void loadAllTenantStorage(Main main, TenantConfig[] tenants) } main.getResourceDistributor().clearAllResourcesWithResourceKey(RESOURCE_KEY); - Set userPoolsInUse = new HashSet<>(); + Set uniquePoolsInUse = new HashSet<>(); for (ResourceDistributor.KeyClass key : resourceKeyToStorageMap.keySet()) { Storage currStorage = resourceKeyToStorageMap.get(key); @@ -259,11 +259,16 @@ public static void loadAllTenantStorage(Main main, TenantConfig[] tenants) main.getResourceDistributor().setResource(key.getTenantIdentifier(), RESOURCE_KEY, new StorageLayer(resourceKeyToStorageMap.get(key))); - userPoolsInUse.add(userPoolId); + uniquePoolsInUse.add(uniqueId); } for (ResourceDistributor.KeyClass key : existingStorageMap.keySet()) { - if (!userPoolsInUse.contains(((StorageLayer) existingStorageMap.get(key)).storage.getUserPoolId())) { + Storage existingStorage = ((StorageLayer) existingStorageMap.get(key)).storage; + String userPoolId = existingStorage.getUserPoolId(); + String connectionPoolId = existingStorage.getConnectionPoolId(); + String uniqueId = userPoolId + "~" + connectionPoolId; + + if (!uniquePoolsInUse.contains(uniqueId)) { ((StorageLayer) existingStorageMap.get(key)).storage.close(); ((StorageLayer) existingStorageMap.get(key)).storage.stopLogging(); } diff --git a/src/main/java/io/supertokens/utils/ConfigMapper.java b/src/main/java/io/supertokens/utils/ConfigMapper.java new file mode 100644 index 000000000..4d7484026 --- /dev/null +++ b/src/main/java/io/supertokens/utils/ConfigMapper.java @@ -0,0 +1,140 @@ +/* + * 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.utils; + +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; +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 { + field.setAccessible(true); + Object convertedValue = convertJsonElementToTargetType(value, field.getType(), field.getName()); + if (convertedValue != null || isNullable(field.getType())) { + try { + field.set(object, convertedValue); + } catch (IllegalAccessException e) { + throw new IllegalStateException("should never happen"); + } + } + } + + 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 + 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"); + } +} diff --git a/src/main/java/io/supertokens/webserver/WebserverAPI.java b/src/main/java/io/supertokens/webserver/WebserverAPI.java index 6b9e23048..03919bc10 100644 --- a/src/main/java/io/supertokens/webserver/WebserverAPI.java +++ b/src/main/java/io/supertokens/webserver/WebserverAPI.java @@ -24,6 +24,7 @@ import io.supertokens.config.CoreConfig; import io.supertokens.exceptions.QuitProgramException; import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; +import io.supertokens.multitenancy.MultitenancyHelper; import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.output.Logging; import io.supertokens.pluginInterface.Storage; @@ -289,15 +290,18 @@ private String getConnectionUriDomain(HttpServletRequest req) throws ServletExce String connectionUriDomain = req.getServerName(); connectionUriDomain = Utils.normalizeAndValidateConnectionUriDomain(connectionUriDomain, false); - try { - if (Config.getConfig(new TenantIdentifier(connectionUriDomain, null, null), main) == - Config.getConfig(new TenantIdentifier(null, null, null), main)) { - return null; + if (MultitenancyHelper.getInstance(main).isConnectionUriDomainPresentInDb(connectionUriDomain)) { + CoreConfig baseConfig = Config.getBaseConfig(main); + if (baseConfig.getSuperTokensLoadOnlyCUD() != null) { + if (!connectionUriDomain.equals(baseConfig.getSuperTokensLoadOnlyCUD())) { + throw new ServletException(new BadRequestException("Connection URI domain is disallowed")); + } } - } catch (TenantOrAppNotFoundException e) { - throw new IllegalStateException(e); + + return connectionUriDomain; } - return connectionUriDomain; + + return null; } @TestOnly @@ -348,7 +352,6 @@ protected AppIdentifierWithStorage getPublicTenantStorage(HttpServletRequest req Storage storage = StorageLayer.getStorage(appIdentifier.getAsPublicTenantIdentifier(), main); return appIdentifier.withStorage(storage); - } protected TenantIdentifierWithStorageAndUserIdMapping getTenantIdentifierWithStorageAndUserIdMappingFromRequest( @@ -493,10 +496,13 @@ protected void service(HttpServletRequest req, HttpServletResponse resp) throws } Logging.info(main, tenantIdentifier, "API ended: " + req.getRequestURI() + ". Method: " + req.getMethod(), false); - try { - RequestStats.getInstance(main, tenantIdentifier.toAppIdentifier()).updateRequestStats(); - } catch (TenantOrAppNotFoundException e) { - // Ignore the error as we would have already sent the response for tenantNotFound + + if (tenantIdentifier != null) { + try { + RequestStats.getInstance(main, tenantIdentifier.toAppIdentifier()).updateRequestStats(); + } catch (TenantOrAppNotFoundException e) { + // Ignore the error as we would have already sent the response for tenantNotFound + } } } diff --git a/src/main/java/io/supertokens/webserver/api/multitenancy/BaseCreateOrUpdate.java b/src/main/java/io/supertokens/webserver/api/multitenancy/BaseCreateOrUpdate.java index 940968568..2fbfb5ce4 100644 --- a/src/main/java/io/supertokens/webserver/api/multitenancy/BaseCreateOrUpdate.java +++ b/src/main/java/io/supertokens/webserver/api/multitenancy/BaseCreateOrUpdate.java @@ -19,6 +19,8 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import io.supertokens.Main; +import io.supertokens.config.Config; +import io.supertokens.config.CoreConfig; import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; import io.supertokens.multitenancy.Multitenancy; import io.supertokens.multitenancy.exception.BadPermissionException; @@ -59,6 +61,14 @@ protected void handle(HttpServletRequest req, TenantIdentifier sourceTenantIdent throw new ServletException(new BadRequestException("requiredSecondaryFactors cannot be empty. Set null instead to remove all required secondary factors.")); } + CoreConfig baseConfig = Config.getBaseConfig(main); + if (baseConfig.getSuperTokensLoadOnlyCUD() != null) { + if (!(targetTenantIdentifier.getConnectionUriDomain().equals(TenantIdentifier.DEFAULT_CONNECTION_URI) || targetTenantIdentifier.getConnectionUriDomain().equals(baseConfig.getSuperTokensLoadOnlyCUD()))) { + throw new ServletException(new BadRequestException("Creation of connection uri domain or app or " + + "tenant is disallowed")); + } + } + TenantConfig tenantConfig = Multitenancy.getTenantInfo(main, new TenantIdentifier(targetTenantIdentifier.getConnectionUriDomain(), targetTenantIdentifier.getAppId(), targetTenantIdentifier.getTenantId())); diff --git a/src/main/java/io/supertokens/webserver/api/passwordless/ConsumeCodeAPI.java b/src/main/java/io/supertokens/webserver/api/passwordless/ConsumeCodeAPI.java index 983dd1624..404947121 100644 --- a/src/main/java/io/supertokens/webserver/api/passwordless/ConsumeCodeAPI.java +++ b/src/main/java/io/supertokens/webserver/api/passwordless/ConsumeCodeAPI.java @@ -97,6 +97,10 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0), createRecipeUserIfNotExists); + io.supertokens.useridmapping.UserIdMapping.populateExternalUserIdForUsers(this.getTenantIdentifierWithStorageFromRequest(req), new AuthRecipeUserInfo[]{consumeCodeResponse.user}); + + ActiveUsers.updateLastActive(this.getPublicTenantStorage(req), main, consumeCodeResponse.user.getSupertokensUserId()); + JsonObject result = new JsonObject(); result.addProperty("status", "OK"); diff --git a/src/test/java/io/supertokens/test/AuthRecipesParallelTest.java b/src/test/java/io/supertokens/test/AuthRecipesParallelTest.java index 3e65fc432..dfd7c78ef 100644 --- a/src/test/java/io/supertokens/test/AuthRecipesParallelTest.java +++ b/src/test/java/io/supertokens/test/AuthRecipesParallelTest.java @@ -20,7 +20,11 @@ import io.supertokens.emailpassword.EmailPassword; import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException; import io.supertokens.emailpassword.exceptions.WrongCredentialsException; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.TestingProcessManager; @@ -37,8 +41,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.*; public class AuthRecipesParallelTest { @Rule @@ -56,50 +59,61 @@ public void beforeEach() { @Test public void timeTakenFor500SignInParallel() throws Exception { - String[] args = {"../"}; + for (int t = 0; t < 5; t++) { + 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; + } + + ExecutorService ex = Executors.newFixedThreadPool(1000); + int numberOfThreads = 500; + + EmailPassword.signUp(process.getProcess(), "test@example.com", "password"); + AtomicInteger counter = new AtomicInteger(0); + AtomicInteger retryCounter = new AtomicInteger(0); + + long st = System.currentTimeMillis(); + for (int i = 0; i < numberOfThreads; i++) { + ex.execute(() -> { + while(true) { + try { + EmailPassword.signIn(process.getProcess(), "test@example.com", "password"); + counter.incrementAndGet(); + break; + } catch (StorageQueryException e) { + retryCounter.incrementAndGet(); + // continue + } catch (WrongCredentialsException e) { + throw new RuntimeException(e); + } + } + }); + } - TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + ex.shutdown(); - if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { - return; - } - - ExecutorService ex = Executors.newFixedThreadPool(1000); - int numberOfThreads = 500; + ex.awaitTermination(2, TimeUnit.MINUTES); + System.out.println("Time taken for " + numberOfThreads + " sign in parallel: " + (System.currentTimeMillis() - st) + "ms"); + System.out.println("Retry counter: " + retryCounter.get()); + assertEquals(counter.get(), numberOfThreads); - EmailPassword.signUp(process.getProcess(), "test@example.com", "password"); - AtomicInteger counter = new AtomicInteger(0); - AtomicInteger retryCounter = new AtomicInteger(0); + if (retryCounter.get() != 0) { + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + continue; // retry + } + assertEquals(0, retryCounter.get()); - long st = System.currentTimeMillis(); - for (int i = 0; i < numberOfThreads; i++) { - ex.execute(() -> { - while(true) { - try { - EmailPassword.signIn(process.getProcess(), "test@example.com", "password"); - counter.incrementAndGet(); - break; - } catch (StorageQueryException e) { - retryCounter.incrementAndGet(); - // continue - } catch (WrongCredentialsException e) { - throw new RuntimeException(e); - } - } - }); + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + return; } - ex.shutdown(); - - ex.awaitTermination(2, TimeUnit.MINUTES); - System.out.println("Time taken for " + numberOfThreads + " sign in parallel: " + (System.currentTimeMillis() - st) + "ms"); - System.out.println("Retry counter: " + retryCounter.get()); - assertEquals(counter.get(), numberOfThreads); - assertEquals(0, retryCounter.get()); - - process.kill(); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + fail(); // tried 5 times } @Test diff --git a/src/test/java/io/supertokens/test/ConfigMapperTest.java b/src/test/java/io/supertokens/test/ConfigMapperTest.java new file mode 100644 index 000000000..1b048020b --- /dev/null +++ b/src/test/java/io/supertokens/test/ConfigMapperTest.java @@ -0,0 +1,296 @@ +/* + * 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.test; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.JsonObject; +import io.supertokens.pluginInterface.exceptions.InvalidConfigException; +import io.supertokens.utils.ConfigMapper; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class ConfigMapperTest { + + public static class DummyConfig { + @JsonProperty + int int_property = -1; + + @JsonProperty + long long_property = -1; + + @JsonProperty + float float_property = -1; + + @JsonProperty + double double_property = -1; + + @JsonProperty + String string_property = "default_string"; + + @JsonProperty + boolean bool_property; + + @JsonProperty + Long nullable_long_property = new Long(-1); + } + + @Test + public void testAllValidConversions() throws Exception { + // Test defaults + { + JsonObject config = new JsonObject(); + assertEquals(-1, ConfigMapper.mapConfig(config, DummyConfig.class).int_property); + assertEquals(-1, ConfigMapper.mapConfig(config, DummyConfig.class).long_property); + assertEquals(-1, ConfigMapper.mapConfig(config, DummyConfig.class).float_property, 0.0001); + assertEquals(-1, ConfigMapper.mapConfig(config, DummyConfig.class).double_property, 0.0001); + assertEquals("default_string", ConfigMapper.mapConfig(config, DummyConfig.class).string_property); + assertEquals(new Long(-1), ConfigMapper.mapConfig(config, DummyConfig.class).nullable_long_property); + } + + // valid for int + { + JsonObject config = new JsonObject(); + config.addProperty("int_property", "100"); + assertEquals(100, ConfigMapper.mapConfig(config, DummyConfig.class).int_property); + } + { + JsonObject config = new JsonObject(); + config.addProperty("int_property", 100); + assertEquals(100, ConfigMapper.mapConfig(config, DummyConfig.class).int_property); + } + + // valid for long + { + JsonObject config = new JsonObject(); + config.addProperty("long_property", "100"); + assertEquals(100, ConfigMapper.mapConfig(config, DummyConfig.class).long_property); + } + { + JsonObject config = new JsonObject(); + config.addProperty("long_property", 100); + assertEquals(100, ConfigMapper.mapConfig(config, DummyConfig.class).long_property); + } + + // valid for float + { + JsonObject config = new JsonObject(); + config.addProperty("float_property", 100); + System.out.println(ConfigMapper.mapConfig(config, DummyConfig.class).float_property); + assertEquals((float) 100, ConfigMapper.mapConfig(config, DummyConfig.class).float_property, 0.001); + } + { + JsonObject config = new JsonObject(); + config.addProperty("float_property", 3.14); + assertEquals((float) 3.14, ConfigMapper.mapConfig(config, DummyConfig.class).float_property, 0.001); + } + { + JsonObject config = new JsonObject(); + config.addProperty("float_property", "100"); + assertEquals((float) 100, ConfigMapper.mapConfig(config, DummyConfig.class).float_property, 0.001); + } + { + JsonObject config = new JsonObject(); + config.addProperty("float_property", "3.14"); + assertEquals((float) 3.14, ConfigMapper.mapConfig(config, DummyConfig.class).float_property, 0.001); + } + + // valid double + { + JsonObject config = new JsonObject(); + config.addProperty("double_property", 100); + assertEquals((double) 100, ConfigMapper.mapConfig(config, DummyConfig.class).double_property, 0.001); + } + { + JsonObject config = new JsonObject(); + config.addProperty("double_property", 3.14); + assertEquals((double) 3.14, ConfigMapper.mapConfig(config, DummyConfig.class).double_property, 0.001); + } + { + JsonObject config = new JsonObject(); + config.addProperty("double_property", "100"); + assertEquals((double) 100, ConfigMapper.mapConfig(config, DummyConfig.class).double_property, 0.001); + } + { + JsonObject config = new JsonObject(); + config.addProperty("double_property", "3.14"); + assertEquals((double) 3.14, ConfigMapper.mapConfig(config, DummyConfig.class).double_property, 0.001); + } + + // valid for bool + { + JsonObject config = new JsonObject(); + config.addProperty("bool_property", "true"); + assertEquals(true, ConfigMapper.mapConfig(config, DummyConfig.class).bool_property); + } + { + JsonObject config = new JsonObject(); + config.addProperty("bool_property", "TRUE"); + assertEquals(true, ConfigMapper.mapConfig(config, DummyConfig.class).bool_property); + } + { + JsonObject config = new JsonObject(); + config.addProperty("bool_property", "false"); + assertEquals(false, ConfigMapper.mapConfig(config, DummyConfig.class).bool_property); + } + { + JsonObject config = new JsonObject(); + config.addProperty("bool_property", true); + assertEquals(true, ConfigMapper.mapConfig(config, DummyConfig.class).bool_property); + } + { + JsonObject config = new JsonObject(); + config.addProperty("bool_property", false); + assertEquals(false, ConfigMapper.mapConfig(config, DummyConfig.class).bool_property); + } + + // valid for string + { + JsonObject config = new JsonObject(); + config.addProperty("string_property", "true"); + assertEquals("true", ConfigMapper.mapConfig(config, DummyConfig.class).string_property); + } + { + JsonObject config = new JsonObject(); + config.addProperty("string_property", true); + assertEquals("true", ConfigMapper.mapConfig(config, DummyConfig.class).string_property); + } + { + JsonObject config = new JsonObject(); + config.addProperty("string_property", 100); + assertEquals("100", ConfigMapper.mapConfig(config, DummyConfig.class).string_property); + } + { + JsonObject config = new JsonObject(); + config.addProperty("string_property", 3.14); + assertEquals("3.14", ConfigMapper.mapConfig(config, DummyConfig.class).string_property); + } + { + JsonObject config = new JsonObject(); + config.addProperty("string_property", "hello"); + assertEquals("hello", ConfigMapper.mapConfig(config, DummyConfig.class).string_property); + } + + { + JsonObject config = new JsonObject(); + config.add("string_property", null); + assertEquals(null, ConfigMapper.mapConfig(config, DummyConfig.class).string_property); + } + + // valid for nullable long + { + JsonObject config = new JsonObject(); + config.add("nullable_long_property", null); + assertEquals(null, ConfigMapper.mapConfig(config, DummyConfig.class).nullable_long_property); + } + + { + JsonObject config = new JsonObject(); + config.addProperty("nullable_long_property", 100); + assertEquals(new Long(100), ConfigMapper.mapConfig(config, DummyConfig.class).nullable_long_property); + } + } + + @Test + public void testInvalidConversions() throws Exception { + String[] properties = new String[]{ + "int_property", + "int_property", + "int_property", + "int_property", + "int_property", + + "long_property", + "long_property", + "long_property", + "long_property", + + "float_property", + "float_property", + "float_property", + + "double_property", + "double_property", + "double_property", + }; + Object[] values = new Object[]{ + "abcd", // int + "", // int + true, // int + new Double(4.5), // int + new Long(1234567892342l), // int + + "abcd", // long + "", // long + true, // long + new Double(4.5), // long + + "abcd", // float + "", // float + true, // float + + "abcd", // double + "", // double + true, // double + }; + + String[] expectedErrorMessages = new String[]{ + "'int_property' must be of type int", // int + "'int_property' must be of type int", // int + "'int_property' must be of type int", // int + "'int_property' must be of type int", // int + "'int_property' must be of type int", // int + + "'long_property' must be of type long", // long + "'long_property' must be of type long", // long + "'long_property' must be of type long", // long + "'long_property' must be of type long", // long + + "'float_property' must be of type float", // float + "'float_property' must be of type float", // float + "'float_property' must be of type float", // float + + "'double_property' must be of type double", // double + "'double_property' must be of type double", // double + "'double_property' must be of type double", // double + }; + + for (int i = 0; i < properties.length; i++) { + try { + System.out.println("Test case " + i); + JsonObject config = new JsonObject(); + if (values[i] == null) { + config.add(properties[i], null); + } + else if (values[i] instanceof String) { + config.addProperty(properties[i], (String) values[i]); + } else if (values[i] instanceof Boolean) { + config.addProperty(properties[i], (Boolean) values[i]); + } else if (values[i] instanceof Number) { + config.addProperty(properties[i], (Number) values[i]); + } else { + throw new RuntimeException("Invalid type"); + } + DummyConfig dc = ConfigMapper.mapConfig(config, DummyConfig.class); + fail(); + } catch (InvalidConfigException e) { + assertEquals(expectedErrorMessages[i], e.getMessage()); + } + } + } +} diff --git a/src/test/java/io/supertokens/test/ConfigTest2_21.java b/src/test/java/io/supertokens/test/ConfigTest2_21.java index 73b886f6f..a4b28c060 100644 --- a/src/test/java/io/supertokens/test/ConfigTest2_21.java +++ b/src/test/java/io/supertokens/test/ConfigTest2_21.java @@ -24,6 +24,7 @@ import org.junit.*; import org.junit.rules.TestRule; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; public class ConfigTest2_21 { @@ -84,4 +85,22 @@ public void testThatNewConfigWorks() throws Exception { EventAndException stopEvent = process.checkOrWaitForEvent(PROCESS_STATE.STOPPED); assertNotNull(stopEvent); } + + @Test + public void testCoreConfigTypeValidationInConfigYaml() throws Exception { + Utils.setValueInConfig("access_token_validity", "abcd"); + + String[] args = { "../" }; + + TestingProcess process = TestingProcessManager.start(args); + + EventAndException startEvent = process.checkOrWaitForEvent(PROCESS_STATE.INIT_FAILURE); + assertNotNull(startEvent); + + assertEquals("io.supertokens.pluginInterface.exceptions.InvalidConfigException: 'access_token_validity' must be of type long", startEvent.exception.getMessage()); + + process.kill(); + EventAndException stopEvent = process.checkOrWaitForEvent(PROCESS_STATE.STOPPED); + assertNotNull(stopEvent); + } } diff --git a/src/test/java/io/supertokens/test/FeatureFlagTest.java b/src/test/java/io/supertokens/test/FeatureFlagTest.java index 98a39851e..408351098 100644 --- a/src/test/java/io/supertokens/test/FeatureFlagTest.java +++ b/src/test/java/io/supertokens/test/FeatureFlagTest.java @@ -94,7 +94,7 @@ public void noLicenseKeyShouldHaveEmptyFeatureFlag() JsonObject stats = FeatureFlag.getInstance(process.getProcess()).getPaidFeatureStats(); Assert.assertEquals(stats.entrySet().size(), 1); - Assert.assertEquals(stats.get("maus").getAsJsonArray().size(), 30); + Assert.assertEquals(stats.get("maus").getAsJsonArray().size(), 31); Assert.assertEquals(stats.get("maus").getAsJsonArray().get(0).getAsInt(), 0); Assert.assertEquals(stats.get("maus").getAsJsonArray().get(29).getAsInt(), 0); @@ -189,7 +189,7 @@ public void testThatCallingGetFeatureFlagAPIReturnsMfaStats() throws Exception { assert features.size() == 2; // MFA + MULTITENANCY } assert features.contains(new JsonPrimitive("mfa")); - assert maus.size() == 30; + assert maus.size() == 31; assert maus.get(0).getAsInt() == 0; assert maus.get(29).getAsInt() == 0; @@ -197,7 +197,7 @@ public void testThatCallingGetFeatureFlagAPIReturnsMfaStats() throws Exception { int totalMfaUsers = mfaStats.get("totalUserCountWithMoreThanOneLoginMethodOrTOTPEnabled").getAsInt(); JsonArray mfaMaus = mfaStats.get("mauWithMoreThanOneLoginMethodOrTOTPEnabled").getAsJsonArray(); - assert mfaMaus.size() == 30; + assert mfaMaus.size() == 31; assert mfaMaus.get(0).getAsInt() == 0; assert mfaMaus.get(29).getAsInt() == 0; @@ -250,7 +250,7 @@ public void testThatCallingGetFeatureFlagAPIReturnsMfaStats() throws Exception { } assert features.contains(new JsonPrimitive("mfa")); - assert maus.size() == 30; + assert maus.size() == 31; assert maus.get(0).getAsInt() == 2; // 2 users have signed up assert maus.get(29).getAsInt() == 2; @@ -258,7 +258,7 @@ public void testThatCallingGetFeatureFlagAPIReturnsMfaStats() throws Exception { int totalMfaUsers = mfaStats.get("totalUserCountWithMoreThanOneLoginMethodOrTOTPEnabled").getAsInt(); JsonArray mfaMaus = mfaStats.get("mauWithMoreThanOneLoginMethodOrTOTPEnabled").getAsJsonArray(); - assert mfaMaus.size() == 30; + assert mfaMaus.size() == 31; assert mfaMaus.get(0).getAsInt() == 1; // only 1 user has TOTP enabled assert mfaMaus.get(29).getAsInt() == 1; @@ -292,7 +292,7 @@ public void testThatCallingGetFeatureFlagAPIReturnsMfaStats() throws Exception { } assert features.contains(new JsonPrimitive("mfa")); - assert maus.size() == 30; + assert maus.size() == 31; assert maus.get(0).getAsInt() == 4; // 2 users have signed up assert maus.get(29).getAsInt() == 4; diff --git a/src/test/java/io/supertokens/test/PathRouterTest.java b/src/test/java/io/supertokens/test/PathRouterTest.java index de865ed16..b58d80664 100644 --- a/src/test/java/io/supertokens/test/PathRouterTest.java +++ b/src/test/java/io/supertokens/test/PathRouterTest.java @@ -1533,7 +1533,9 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I public void tenantNotFoundTest3() throws InterruptedException, IOException, io.supertokens.httpRequest.HttpResponseException, InvalidConfigException, - io.supertokens.test.httpRequest.HttpResponseException, TenantOrAppNotFoundException { + io.supertokens.test.httpRequest.HttpResponseException, TenantOrAppNotFoundException, + InvalidProviderConfigException, StorageQueryException, FeatureNotEnabledException, + CannotModifyBaseConfigException, BadPermissionException { String[] args = {"../"}; Utils.setValueInConfig("host", "\"0.0.0.0\""); @@ -1556,15 +1558,26 @@ public void tenantNotFoundTest3() StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess()) .modifyConfigToAddANewUserPoolForTesting(tenantConfig, 2); - Config.loadAllTenantConfig(process.getProcess(), new TenantConfig[]{ - new TenantConfig(new TenantIdentifier("localhost", null, null), new EmailPasswordConfig(false), + Multitenancy.addNewOrUpdateAppOrTenant( + process.getProcess(), + new TenantConfig( + new TenantIdentifier("localhost", null, null), + new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), null, null, tenantConfig), - new TenantConfig(new TenantIdentifier("localhost", null, "t1"), new EmailPasswordConfig(false), + false + ); + Multitenancy.addNewOrUpdateAppOrTenant( + process.getProcess(), + new TenantConfig( + new TenantIdentifier("localhost", null, "t1"), + new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - null, null, tenantConfig)}, new ArrayList<>()); + null, null, tenantConfig), + false + ); Webserver.getInstance(process.getProcess()).addAPI(new WebserverAPI(process.getProcess(), "") { @@ -2788,7 +2801,9 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I public void tenantNotFoundWithAppIdTest3() throws InterruptedException, IOException, io.supertokens.httpRequest.HttpResponseException, InvalidConfigException, - io.supertokens.test.httpRequest.HttpResponseException, TenantOrAppNotFoundException { + io.supertokens.test.httpRequest.HttpResponseException, TenantOrAppNotFoundException, + InvalidProviderConfigException, StorageQueryException, FeatureNotEnabledException, + CannotModifyBaseConfigException, BadPermissionException { String[] args = {"../"}; Utils.setValueInConfig("host", "\"0.0.0.0\""); @@ -2811,15 +2826,26 @@ public void tenantNotFoundWithAppIdTest3() StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess()) .modifyConfigToAddANewUserPoolForTesting(tenantConfig, 2); - Config.loadAllTenantConfig(process.getProcess(), new TenantConfig[]{ - new TenantConfig(new TenantIdentifier("localhost", null, null), new EmailPasswordConfig(false), + Multitenancy.addNewOrUpdateAppOrTenant( + process.getProcess(), + new TenantConfig( + new TenantIdentifier("localhost", null, null), + new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), null, null, tenantConfig), - new TenantConfig(new TenantIdentifier("localhost", "app1", "t1"), new EmailPasswordConfig(false), + false + ); + Multitenancy.addNewOrUpdateAppOrTenant( + process.getProcess(), + new TenantConfig( + new TenantIdentifier("localhost", "app1", "t1"), + new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - null, null, tenantConfig)}, new ArrayList<>()); + null, null, tenantConfig), + false + ); Webserver.getInstance(process.getProcess()).addAPI(new WebserverAPI(process.getProcess(), "") { diff --git a/src/test/java/io/supertokens/test/StorageLayerTest.java b/src/test/java/io/supertokens/test/StorageLayerTest.java index eb88558d8..d649cc7fb 100644 --- a/src/test/java/io/supertokens/test/StorageLayerTest.java +++ b/src/test/java/io/supertokens/test/StorageLayerTest.java @@ -97,7 +97,7 @@ public void totpCodeLengthTest() throws Exception { // This error will be different in Postgres and MySQL // We added (CHECK (LENGTH(code) <= 8)) to the table definition in SQLite String totpUsedCodeTable = Config.getConfig(start).getTotpUsedCodesTable(); - assert e.getMessage().contains("CHECK constraint failed: " + totpUsedCodeTable) || e.getMessage().contains("LENGTH(code) <= 8"); + assert e.getMessage().contains("CHECK constraint failed: "); } // Try code with length < 8 diff --git a/src/test/java/io/supertokens/test/TelemetryTest.java b/src/test/java/io/supertokens/test/TelemetryTest.java index 95e312ac7..968d154f4 100644 --- a/src/test/java/io/supertokens/test/TelemetryTest.java +++ b/src/test/java/io/supertokens/test/TelemetryTest.java @@ -21,7 +21,10 @@ import io.supertokens.ProcessState; import io.supertokens.ProcessState.PROCESS_STATE; import io.supertokens.cronjobs.telemetry.Telemetry; +import io.supertokens.dashboard.Dashboard; import io.supertokens.httpRequest.HttpRequestMocking; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.TestingProcessManager.TestingProcess; import io.supertokens.version.Version; import org.junit.AfterClass; @@ -111,6 +114,16 @@ public void testThatTelemetryWorks() throws Exception { String[] args = { "../" }; TestingProcess process = TestingProcessManager.start(args, false); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getBaseStorage(process.getProcess()).getType() == STORAGE_TYPE.SQL) { + Dashboard.signUpDashboardUser(process.getProcess(), "test@example.com", "password123"); + } + + // Restarting the process to send telemetry again + process.kill(false); + process = TestingProcessManager.start(args, false); ByteArrayOutputStream output = new ByteArrayOutputStream(); final HttpURLConnection mockCon = mock(HttpURLConnection.class); @@ -149,13 +162,26 @@ protected URLConnection openConnection(URL u) { assertNotNull(process.checkOrWaitForEvent(PROCESS_STATE.SENT_TELEMETRY)); JsonObject telemetryData = new JsonParser().parse(output.toString()).getAsJsonObject(); + assertEquals(7, telemetryData.entrySet().size()); assertTrue(telemetryData.has("telemetryId")); assertEquals(telemetryData.get("superTokensVersion").getAsString(), Version.getVersion(process.getProcess()).getCoreVersion()); assertEquals(telemetryData.get("appId").getAsString(), "public"); assertEquals(telemetryData.get("connectionUriDomain").getAsString(), ""); - assertTrue(telemetryData.has("mau")); + assertTrue(telemetryData.has("maus")); + assertTrue(telemetryData.has("dashboardUserEmails")); + + if (StorageLayer.getBaseStorage(process.getProcess()).getType() == STORAGE_TYPE.SQL) { + assertEquals(1, telemetryData.get("dashboardUserEmails").getAsJsonArray().size()); + assertEquals("test@example.com", telemetryData.get("dashboardUserEmails").getAsJsonArray().get(0).getAsString()); + assertEquals(31, telemetryData.get("maus").getAsJsonArray().size()); + assertEquals(0, telemetryData.get("usersCount").getAsInt()); + } else { + assertEquals(0, telemetryData.get("dashboardUserEmails").getAsJsonArray().size()); + assertEquals(0, telemetryData.get("maus").getAsJsonArray().size()); + assertEquals(-1, telemetryData.get("usersCount").getAsInt()); + } process.kill(); assertNotNull(process.checkOrWaitForEvent(PROCESS_STATE.STOPPED)); diff --git a/src/test/java/io/supertokens/test/accountlinking/TestGetUserSpeed.java b/src/test/java/io/supertokens/test/accountlinking/TestGetUserSpeed.java index de6a8f890..237e26ac1 100644 --- a/src/test/java/io/supertokens/test/accountlinking/TestGetUserSpeed.java +++ b/src/test/java/io/supertokens/test/accountlinking/TestGetUserSpeed.java @@ -59,18 +59,8 @@ public void beforeEach() { Utils.reset(); } - @Test - public void testUserCreationLinkingAndGetByIdSpeeds() throws Exception { - String[] args = {"../"}; - TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); - Utils.setValueInConfig("postgresql_connection_pool_size", "100"); - Utils.setValueInConfig("mysql_connection_pool_size", "100"); - - 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)); + public void testUserCreationLinkingAndGetByIdSpeedsCommon(TestingProcessManager.TestingProcess process, + long createTime, long linkingTime, long getTime) throws Exception { if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { return; @@ -81,6 +71,7 @@ public void testUserCreationLinkingAndGetByIdSpeeds() throws Exception { } int numberOfUsers = 10000; + List userIds = new ArrayList<>(); List userIds2 = new ArrayList<>(); Lock lock = new ReentrantLock(); @@ -108,7 +99,7 @@ public void testUserCreationLinkingAndGetByIdSpeeds() throws Exception { long end = System.currentTimeMillis(); System.out.println("Created users " + numberOfUsers + " in " + (end - start) + "ms"); - assert end - start < 25000; // 25 sec + assert end - start < createTime; // 25 sec } Thread.sleep(10000); // wait for index @@ -148,7 +139,7 @@ public void testUserCreationLinkingAndGetByIdSpeeds() throws Exception { es.awaitTermination(5, TimeUnit.MINUTES); long end = System.currentTimeMillis(); System.out.println("Accounts linked in " + (end - start) + "ms"); - assert end - start < 50000; // 50 sec + assert end - start < linkingTime; // 50 sec } Thread.sleep(10000); // wait for index @@ -169,10 +160,44 @@ public void testUserCreationLinkingAndGetByIdSpeeds() throws Exception { es.awaitTermination(5, TimeUnit.MINUTES); long end = System.currentTimeMillis(); System.out.println("Time taken for " + numberOfUsers + " users: " + (end - start) + "ms"); - assert end - start < 20000; // 20 sec + assert end - start < getTime; // 20 sec } process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } + + @Test + public void testUserCreationLinkingAndGetByIdSpeedsWithoutMinIdle() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + Utils.setValueInConfig("postgresql_connection_pool_size", "100"); + Utils.setValueInConfig("mysql_connection_pool_size", "100"); + + 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)); + + testUserCreationLinkingAndGetByIdSpeedsCommon(process, 25000, 50000, 20000); + } + + @Test + public void testUserCreationLinkingAndGetByIdSpeedsWithMinIdle() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + Utils.setValueInConfig("postgresql_connection_pool_size", "100"); + Utils.setValueInConfig("mysql_connection_pool_size", "100"); + Utils.setValueInConfig("postgresql_minimum_idle_connections", "1"); + Utils.setValueInConfig("mysql_minimum_idle_connections", "1"); + + 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)); + + testUserCreationLinkingAndGetByIdSpeedsCommon(process, 60000, 50000, 20000); + } } diff --git a/src/test/java/io/supertokens/test/accountlinking/api/UserPaginationTest.java b/src/test/java/io/supertokens/test/accountlinking/api/UserPaginationTest.java index 99cf76376..bb2bf18be 100644 --- a/src/test/java/io/supertokens/test/accountlinking/api/UserPaginationTest.java +++ b/src/test/java/io/supertokens/test/accountlinking/api/UserPaginationTest.java @@ -17,6 +17,7 @@ package io.supertokens.test.accountlinking.api; import com.google.gson.JsonArray; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import io.supertokens.Main; import io.supertokens.ProcessState; @@ -50,8 +51,8 @@ import java.security.NoSuchAlgorithmException; import java.util.*; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.*; +import static org.junit.Assert.assertTrue; public class UserPaginationTest { @Rule @@ -380,4 +381,43 @@ public void testUserPaginationWithManyUsers() throws Exception { process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } + + @Test + public void testUserPaginationFromOldVersion() 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; + } + + AuthRecipeUserInfo user1 = createEmailPasswordUser(process.getProcess(), "test@example.com", "password1"); + Thread.sleep(50); + AuthRecipeUserInfo user2 = createThirdPartyUser(process.getProcess(), "google", "userid1", "test@example.com"); + Thread.sleep(50); + AuthRecipeUserInfo user3 = createPasswordlessUserWithEmail(process.getProcess(), "test@example.com"); + Thread.sleep(50); + + AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), user2.getSupertokensUserId()).user; + AuthRecipe.linkAccounts(process.getProcess(), user1.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), user3.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + + Map params = new HashMap<>(); + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/users", params, 1000, 1000, null, + SemVer.v3_0.get(), ""); + + assertEquals(1, response.get("users").getAsJsonArray().size()); + JsonObject user = response.get("users").getAsJsonArray().get(0).getAsJsonObject().get("user").getAsJsonObject(); + + assertEquals(user1.getSupertokensUserId(), user.get("id").getAsString()); // oldest login method + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } } diff --git a/src/test/java/io/supertokens/test/dashboard/DashboardTest.java b/src/test/java/io/supertokens/test/dashboard/DashboardTest.java index 7bb9211a4..eeaa3b447 100644 --- a/src/test/java/io/supertokens/test/dashboard/DashboardTest.java +++ b/src/test/java/io/supertokens/test/dashboard/DashboardTest.java @@ -290,7 +290,7 @@ public void testDashboardUsageStats() throws Exception { JsonObject usageStats = response.get("usageStats").getAsJsonObject(); JsonArray mauArr = usageStats.get("maus").getAsJsonArray(); assertEquals(1, usageStats.entrySet().size()); - assertEquals(30, mauArr.size()); + assertEquals(31, mauArr.size()); assertEquals(0, mauArr.get(0).getAsInt()); assertEquals(0, mauArr.get(29).getAsInt()); } @@ -312,7 +312,7 @@ public void testDashboardUsageStats() throws Exception { JsonObject usageStats = response.get("usageStats").getAsJsonObject(); JsonArray mauArr = usageStats.get("maus").getAsJsonArray(); assertEquals(1, usageStats.entrySet().size()); - assertEquals(30, mauArr.size()); + assertEquals(31, mauArr.size()); assertEquals(0, mauArr.get(0).getAsInt()); assertEquals(0, mauArr.get(29).getAsInt()); } @@ -338,7 +338,7 @@ public void testDashboardUsageStats() throws Exception { JsonObject usageStats = response.get("usageStats").getAsJsonObject(); JsonObject dashboardLoginObject = usageStats.get("dashboard_login").getAsJsonObject(); assertEquals(2, usageStats.entrySet().size()); - assertEquals(30, usageStats.get("maus").getAsJsonArray().size()); + assertEquals(31, usageStats.get("maus").getAsJsonArray().size()); assertEquals(1, dashboardLoginObject.entrySet().size()); assertEquals(1, dashboardLoginObject.get("user_count").getAsInt()); } @@ -366,7 +366,7 @@ public void testDashboardUsageStats() throws Exception { JsonObject usageStats = response.get("usageStats").getAsJsonObject(); JsonObject dashboardLoginObject = usageStats.get("dashboard_login").getAsJsonObject(); assertEquals(2, usageStats.entrySet().size()); - assertEquals(30, usageStats.get("maus").getAsJsonArray().size()); + assertEquals(31, usageStats.get("maus").getAsJsonArray().size()); assertEquals(1, dashboardLoginObject.entrySet().size()); assertEquals(4, dashboardLoginObject.get("user_count").getAsInt()); } diff --git a/src/test/java/io/supertokens/test/multitenant/ConfigTest.java b/src/test/java/io/supertokens/test/multitenant/ConfigTest.java index 421e3b542..23675314c 100644 --- a/src/test/java/io/supertokens/test/multitenant/ConfigTest.java +++ b/src/test/java/io/supertokens/test/multitenant/ConfigTest.java @@ -1914,6 +1914,7 @@ public void testAllConflictingConfigs() throws Exception { "argon2_memory_kb", "argon2_parallelism", "bcrypt_log_rounds", + "supertokens_saas_load_only_cud" }; Object[] disallowedValues = new Object[]{ 3567, // port @@ -1930,6 +1931,7 @@ public void testAllConflictingConfigs() throws Exception { 87795, // argon2_memory_kb 2, // argon2_parallelism 11, // bcrypt_log_rounds + "mydomain.com", // supertokens_saas_load_only_cud }; process.kill(); @@ -1995,7 +1997,7 @@ public void testAllConflictingConfigs() throws Exception { new Object[]{true, false}, // disable_telemetry new Object[]{"BCRYPT", "ARGON2"}, // password_hashing_alg new Object[]{"abcd1234abcd1234abcd1234abcd1234", "qwer1234qwer1234qwer1234qwer1234"}, // firebase_password_hashing_signer_key - new Object[]{"2.21", "3.0"} // supertokens_max_cdi_version + new Object[]{"2.21", "3.0"}, // supertokens_max_cdi_version }; for (int i=0; i> uniqueUserPoolIdsTenants = StorageLayer.getTenantsWithUniqueUserPoolId(process.getProcess()); + Cronjobs.addCronjob(process.getProcess(), LoadOnlyCUDTest.PerAppCronjob.getInstance(process.getProcess(), uniqueUserPoolIdsTenants)); + + Thread.sleep(3000); + Set appIdentifiersFromCron = PerAppCronjob.getInstance(process.getProcess(), uniqueUserPoolIdsTenants).appIdentifiers; + assertEquals(2, appIdentifiersFromCron.size()); + for (AppIdentifier app : appIdentifiersFromCron) { + assertNotEquals("localhost.org", app.getConnectionUriDomain()); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + static class PerAppCronjob extends CronTask { + private static final String RESOURCE_ID = "io.supertokens.test.CronjobTest.NormalCronjob"; + + private PerAppCronjob(Main main, List> tenantsInfo) { + super("PerTenantCronjob", main, tenantsInfo, true); + } + + Set appIdentifiers = new HashSet<>(); + + public static LoadOnlyCUDTest.PerAppCronjob getInstance(Main main, List> tenantsInfo) { + try { + return (LoadOnlyCUDTest.PerAppCronjob) main.getResourceDistributor().getResource(new TenantIdentifier(null, null, null), RESOURCE_ID); + } catch (TenantOrAppNotFoundException e) { + return (LoadOnlyCUDTest.PerAppCronjob) main.getResourceDistributor() + .setResource(new TenantIdentifier(null, null, null), RESOURCE_ID, new LoadOnlyCUDTest.PerAppCronjob(main, tenantsInfo)); + } + } + + @Override + public int getIntervalTimeSeconds() { + return 1; + } + + @Override + public int getInitialWaitTimeSeconds() { + return 0; + } + + @Override + protected void doTaskPerApp(AppIdentifier app) throws Exception { + appIdentifiers.add(app); + } + } +} \ No newline at end of file