From 9f7cd287ebe63a252ec7817687fd0f3abc43bf9f Mon Sep 17 00:00:00 2001 From: Prateek Surana Date: Fri, 23 Feb 2024 18:07:17 +0530 Subject: [PATCH] feat: Implement method to get plugin config properties (#186) * Add a new method to get the config as json * Update changelog * Remove the check for protected config from method * Revert version changes * Update descriptions * Update config properties descriptions to match those in the yaml files * Throw error when invalid fields * Refactor getConfigFieldsInfo method in Start and PostgreSQLConfig classes --- CHANGELOG.md | 2 + config.yaml | 2 +- devConfig.yaml | 2 +- .../supertokens/storage/postgresql/Start.java | 8 +- .../annotations/ConfigDescription.java | 29 ++++ .../postgresql/config/PostgreSQLConfig.java | 64 +++++++ .../test/PostgresSQLConfigTest.java | 162 ++++++++++++++++++ 7 files changed, 265 insertions(+), 4 deletions(-) create mode 100644 src/main/java/io/supertokens/storage/postgresql/annotations/ConfigDescription.java create mode 100644 src/test/java/io/supertokens/storage/postgresql/test/PostgresSQLConfigTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f3aa6c8..e638d03f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +- Adds implementation for a new method `getConfigFieldsInfo` to fetch the plugin config fields. + ## [5.0.8] - 2024-02-19 - Fixes vulnerabilities in dependencies diff --git a/config.yaml b/config.yaml index 38ade78f..254bd14a 100644 --- a/config.yaml +++ b/config.yaml @@ -66,7 +66,7 @@ postgresql_config_version: 0 # (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "thirdparty_users") string value. Specify the name of the table # that will store the thirdparty recipe users. -# postgresql_thirdparty_users_table_name +# postgresql_thirdparty_users_table_name: # (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: 60000) long value. Timeout in milliseconds for the idle connections # to be closed. diff --git a/devConfig.yaml b/devConfig.yaml index a25dba97..a394274f 100644 --- a/devConfig.yaml +++ b/devConfig.yaml @@ -68,7 +68,7 @@ postgresql_password: "root" # (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "thirdparty_users") string value. Specify the name of the table # that will store the thirdparty recipe users. -# postgresql_thirdparty_users_table_name +# postgresql_thirdparty_users_table_name: # (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: 60000) long value. Timeout in milliseconds for the idle connections # to be closed. diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 86a7e876..e3b21b32 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -117,8 +117,7 @@ public class Start // SaaS. If the core is not running in SuperTokens SaaS, this array has no effect. private static String[] PROTECTED_DB_CONFIG = new String[]{"postgresql_connection_pool_size", "postgresql_connection_uri", "postgresql_host", "postgresql_port", "postgresql_user", "postgresql_password", - "postgresql_database_name", "postgresql_table_schema", "postgresql_idle_connection_timeout", - "postgresql_minimum_idle_connections"}; + "postgresql_database_name", "postgresql_table_schema"}; private static final Object appenderLock = new Object(); public static boolean silent = false; private ResourceDistributor resourceDistributor = new ResourceDistributor(); @@ -2784,6 +2783,11 @@ public Set getValidFieldsInConfig() { return PostgreSQLConfig.getValidFields(); } + @Override + public ArrayList getConfigFieldsInfo() { + return PostgreSQLConfig.getConfigFieldsInfo(); + } + @Override public void setLogLevels(Set logLevels) { Config.setLogLevels(this, logLevels); diff --git a/src/main/java/io/supertokens/storage/postgresql/annotations/ConfigDescription.java b/src/main/java/io/supertokens/storage/postgresql/annotations/ConfigDescription.java new file mode 100644 index 00000000..762ae6eb --- /dev/null +++ b/src/main/java/io/supertokens/storage/postgresql/annotations/ConfigDescription.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + + package io.supertokens.storage.postgresql.annotations; + + import java.lang.annotation.ElementType; + import java.lang.annotation.Retention; + import java.lang.annotation.RetentionPolicy; + import java.lang.annotation.Target; + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + public @interface ConfigDescription { + String value() default ""; + } + \ No newline at end of file diff --git a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java index 2c464849..32a182ec 100644 --- a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java +++ b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java @@ -22,14 +22,18 @@ import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; import com.google.gson.JsonObject; + +import io.supertokens.pluginInterface.ConfigFieldInfo; import io.supertokens.pluginInterface.exceptions.InvalidConfigException; import io.supertokens.storage.postgresql.annotations.ConnectionPoolProperty; import io.supertokens.storage.postgresql.annotations.IgnoreForAnnotationCheck; import io.supertokens.storage.postgresql.annotations.NotConflictingWithinUserPool; +import io.supertokens.storage.postgresql.annotations.ConfigDescription; import io.supertokens.storage.postgresql.annotations.UserPoolProperty; import java.lang.reflect.Field; import java.net.URI; +import java.util.ArrayList; import java.util.HashSet; import java.util.Map; import java.util.Objects; @@ -40,84 +44,105 @@ public class PostgreSQLConfig { @JsonProperty @IgnoreForAnnotationCheck + @ConfigDescription("The version of the config.") private int postgresql_config_version = -1; @JsonProperty @ConnectionPoolProperty + @ConfigDescription("Defines the connection pool size to PostgreSQL. (Default: 10)") private int postgresql_connection_pool_size = 10; @JsonProperty @UserPoolProperty + @ConfigDescription("Specify the postgresql host url here. (Default: localhost)") private String postgresql_host = null; @JsonProperty @UserPoolProperty + @ConfigDescription("Specify the port to use when connecting to PostgreSQL instance. (Default: 5432)") private int postgresql_port = -1; @JsonProperty @ConnectionPoolProperty + @ConfigDescription("The PostgreSQL user to use to query the database. If the relevant tables are not already created by you, this user should have the ability to create new tables. To see the tables needed, visit: https://supertokens.io/docs/community/getting-started/database-setup/postgresql") private String postgresql_user = null; @JsonProperty @ConnectionPoolProperty + @ConfigDescription("Password for the PostgreSQL user. If you have not set a password make this an empty string.") private String postgresql_password = null; @JsonProperty @UserPoolProperty + @ConfigDescription("The database name to store SuperTokens related data. (Default: supertokens)") private String postgresql_database_name = null; @JsonProperty @NotConflictingWithinUserPool + @ConfigDescription("A prefix to add to all table names managed by SuperTokens. An \"_\" will be added between this prefix and the actual table name if the prefix is defined. (Default: \"\")") private String postgresql_table_names_prefix = ""; @JsonProperty @NotConflictingWithinUserPool + @ConfigDescription("Specify the name of the table that will store secret keys and app info necessary for the functioning sessions. (Default: \"key_value\")") private String postgresql_key_value_table_name = null; @JsonProperty @NotConflictingWithinUserPool + @ConfigDescription("Specify the name of the table that will store the session info for users. (Default: \"session_info\")") private String postgresql_session_info_table_name = null; @JsonProperty @NotConflictingWithinUserPool + @ConfigDescription("Specify the name of the table that will store the user information, along with their email and hashed password. (Default: \"emailpassword_users\")") private String postgresql_emailpassword_users_table_name = null; @JsonProperty @NotConflictingWithinUserPool + @ConfigDescription("Specify the name of the table that will store the password reset tokens for users. (Default: \"emailpassword_pswd_reset_tokens\")") private String postgresql_emailpassword_pswd_reset_tokens_table_name = null; @JsonProperty @NotConflictingWithinUserPool + @ConfigDescription("Specify the name of the table that will store the email verification tokens for users. (Default: \"emailverification_tokens\")") private String postgresql_emailverification_tokens_table_name = null; @JsonProperty @NotConflictingWithinUserPool + @ConfigDescription("Specify the name of the table that will store the verified email addresses. (Default: \"emailverification_verified_emails\")") private String postgresql_emailverification_verified_emails_table_name = null; @JsonProperty @NotConflictingWithinUserPool + @ConfigDescription("Specify the name of the table that will store the thirdparty recipe users. (Default: \"thirdparty_users\")") private String postgresql_thirdparty_users_table_name = null; @JsonProperty @UserPoolProperty + @ConfigDescription("The schema for tables. (Default: public)") private String postgresql_table_schema = "public"; @JsonProperty @IgnoreForAnnotationCheck + @ConfigDescription("Specify the PostgreSQL connection URI in the following format: postgresql://[user[:[password]]@]host[:port][/dbname][?attr1=val1&attr2=val2... Values provided via other configs will override values provided by this config. (Default: null)") private String postgresql_connection_uri = null; @ConnectionPoolProperty + @ConfigDescription("The connection attributes of the PostgreSQL database.") private String postgresql_connection_attributes = "allowPublicKeyRetrieval=true"; @ConnectionPoolProperty + @ConfigDescription("The scheme of the PostgreSQL database.") private String postgresql_connection_scheme = "postgresql"; @JsonProperty @ConnectionPoolProperty + @ConfigDescription("Timeout in milliseconds for the idle connections to be closed. (Default: 60000)") private long postgresql_idle_connection_timeout = 60000; @JsonProperty @ConnectionPoolProperty + @ConfigDescription("Minimum number of idle connections to be kept active. If not set, minimum idle connections will be same as the connection pool size. (Default: null)") private Integer postgresql_minimum_idle_connections = null; @IgnoreForAnnotationCheck @@ -134,6 +159,45 @@ public static Set getValidFields() { return validFields; } + public static ArrayList getConfigFieldsInfo() { + ArrayList result = new ArrayList(); + + for (String fieldId : PostgreSQLConfig.getValidFields()) { + try { + Field field = PostgreSQLConfig.class.getDeclaredField(fieldId); + if (!field.isAnnotationPresent(JsonProperty.class)) { + continue; + } + + String name = field.getName(); + String description = field.isAnnotationPresent(ConfigDescription.class) + ? field.getAnnotation(ConfigDescription.class).value() + : ""; + boolean isDifferentAcrossTenants = true; + + String type = null; + + Class fieldType = field.getType(); + + if (fieldType == String.class) { + type = "string"; + } else if (fieldType == boolean.class) { + type = "boolean"; + } else if (fieldType == int.class || fieldType == long.class || fieldType == Integer.class) { + type = "number"; + } else { + throw new RuntimeException("Unknown field type " + fieldType.getName()); + } + + result.add(new ConfigFieldInfo(name, description, isDifferentAcrossTenants, type)); + + } catch (NoSuchFieldException e) { + continue; + } + } + return result; + } + public String getTableSchema() { return postgresql_table_schema; } diff --git a/src/test/java/io/supertokens/storage/postgresql/test/PostgresSQLConfigTest.java b/src/test/java/io/supertokens/storage/postgresql/test/PostgresSQLConfigTest.java new file mode 100644 index 00000000..ee23eb0e --- /dev/null +++ b/src/test/java/io/supertokens/storage/postgresql/test/PostgresSQLConfigTest.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + */ + +package io.supertokens.storage.postgresql.test; + +import io.supertokens.ProcessState; +import io.supertokens.storage.postgresql.annotations.ConfigDescription; +import io.supertokens.storage.postgresql.config.PostgreSQLConfig; + +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import static org.junit.Assert.*; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +public class PostgresSQLConfigTest { + + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + @Test + public void testAllConfigAreReturnedCorrectly() throws Exception { + PostgreSQLConfig.getConfigFieldsInfo(); + } + + @Test + public void testMatchConfigPropertiesDescription() throws Exception { + String[] args = { "../" }; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + // Skipping postgresql_config_version because it doesn't + // have a description in the config.yaml file + String[] ignoredProperties = { "postgresql_config_version" }; + + // Match the descriptions in the config.yaml file with the descriptions in the + // CoreConfig class + matchYamlAndConfigDescriptions("./config.yaml", ignoredProperties); + + // Match the descriptions in the devConfig.yaml file with the descriptions in + // the CoreConfig class + String[] devConfigIgnoredProperties = Arrays.copyOf(ignoredProperties, ignoredProperties.length + 2); + // We ignore these properties in devConfig.yaml because it has a different + // description + // in devConfig.yaml and has a default value + devConfigIgnoredProperties[ignoredProperties.length] = "postgresql_user"; + devConfigIgnoredProperties[ignoredProperties.length + 1] = "postgresql_password"; + matchYamlAndConfigDescriptions("./devConfig.yaml", devConfigIgnoredProperties); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + private void matchYamlAndConfigDescriptions(String path, String[] ignoreProperties) throws Exception { + try (BufferedReader reader = new BufferedReader(new FileReader(path))) { + // Get the content of the file as string + String content = reader.lines().collect(Collectors.joining(System.lineSeparator())); + // Find the line that contains 'postgresql_config_version', and then split + // the file after that line + String allProperties = content.split("postgresql_config_version:\\s*\\d+\n")[1]; + + // Split by all the other allProperties string by new line + String[] properties = allProperties.split("\n\n"); + // This will contain the description of each property from the yaml file + Map propertyDescriptions = new HashMap(); + + System.out.println("Last property: " + properties[properties.length - 1] + "\n\n"); + + for (int i = 0; i < properties.length; i++) { + String possibleProperty = properties[i].trim(); + String[] lines = possibleProperty.split("\n"); + // This ensures that it is a property with a description as a comment + // at the top + if (lines[lines.length - 1].endsWith(":")) { + String propertyKeyString = lines[lines.length - 1]; + // Remove the comment "# " from the start + String propertyKey = propertyKeyString.substring(2, propertyKeyString.length() - 1); + String propertyDescription = ""; + // Remove the comment "# " from the start and merge all the lines to form the + // description + for (int j = 0; j < lines.length - 1; j++) { + propertyDescription = propertyDescription + " " + lines[j].substring(2); + } + propertyDescription = propertyDescription.trim(); + + propertyDescriptions.put(propertyKey, propertyDescription); + } + } + + for (String fieldId : PostgreSQLConfig.getValidFields()) { + if (Arrays.asList(ignoreProperties).contains(fieldId)) { + continue; + } + + Field field = PostgreSQLConfig.class.getDeclaredField(fieldId); + + // Skip fields that are not annotated with JsonProperty + if (!field.isAnnotationPresent(JsonProperty.class)) { + continue; + } + + String descriptionInConfig = field.getAnnotation(ConfigDescription.class).value(); + String descriptionInYaml = propertyDescriptions.get(fieldId); + + if (descriptionInYaml == null) { + fail("Unable to find description or property for " + fieldId + " in " + path + " file"); + } + + // Remove the default value from config, since we add default value at the end + // config description + descriptionInConfig = descriptionInConfig.replaceAll("\\s\\[Default:.*|\\s\\(Default:.*", "").trim(); + // Remove period from end if present, since not all descriptions in + // config.yaml have that + descriptionInConfig = descriptionInConfig.replaceAll("\\.$", "").trim(); + + // Assert that description in yaml contains the description in config + if (!descriptionInYaml.contains(descriptionInConfig)) { + fail("Description in config class for " + fieldId + " does not match description in " + path + + " file"); + } + } + } + } + +}