diff --git a/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java b/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java index 003b8ddde36..168eed71840 100644 --- a/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java +++ b/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java @@ -9,9 +9,11 @@ import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException; import com.appsmith.external.helpers.DataTypeServiceUtils; import com.appsmith.external.helpers.MustacheHelper; +import com.appsmith.external.helpers.SSHUtils; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionExecutionRequest; import com.appsmith.external.models.ActionExecutionResult; +import com.appsmith.external.models.ConnectionContext; import com.appsmith.external.models.DBAuth; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.DatasourceStructure; @@ -22,6 +24,7 @@ import com.appsmith.external.models.Property; import com.appsmith.external.models.PsParameterDTO; import com.appsmith.external.models.RequestParamDTO; +import com.appsmith.external.models.SSHConnection; import com.appsmith.external.models.SSLDetails; import com.appsmith.external.plugins.BasePlugin; import com.appsmith.external.plugins.PluginExecutor; @@ -83,10 +86,17 @@ import java.util.stream.Stream; import static com.appsmith.external.constants.ActionConstants.ACTION_CONFIGURATION_BODY; +import static com.appsmith.external.constants.PluginConstants.HostName.LOCALHOST; import static com.appsmith.external.constants.PluginConstants.PluginName.POSTGRES_PLUGIN_NAME; +import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.DS_INVALID_SSH_HOSTNAME_ERROR_MSG; +import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.DS_MISSING_SSH_HOSTNAME_ERROR_MSG; +import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.DS_MISSING_SSH_KEY_ERROR_MSG; +import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.DS_MISSING_SSH_USERNAME_ERROR_MSG; import static com.appsmith.external.helpers.PluginUtils.getColumnsListForJdbcPlugin; import static com.appsmith.external.helpers.PluginUtils.getIdenticalColumns; import static com.appsmith.external.helpers.PluginUtils.getPSParamLabel; +import static com.appsmith.external.helpers.SSHUtils.getConnectionContext; +import static com.appsmith.external.helpers.SSHUtils.isSSHEnabled; import static com.appsmith.external.helpers.Sizeof.sizeof; import static com.appsmith.external.helpers.SmartSubstitutionHelper.replaceQuestionMarkWithDollarIndex; import static com.external.plugins.utils.PostgresDataTypeUtils.DataType.BOOL; @@ -135,6 +145,8 @@ public class PostgresPlugin extends BasePlugin { private static int MAX_SIZE_SUPPORTED; + private static final int CONNECTION_METHOD_INDEX = 1; + public static PostgresDatasourceUtils postgresDatasourceUtils = new PostgresDatasourceUtils(); public PostgresPlugin(PluginWrapper wrapper) { @@ -297,6 +309,7 @@ public ActionConfiguration getSchemaPreviewActionConfig(Template queryTemplate, @Override public Mono getEndpointIdentifierForRateLimit(DatasourceConfiguration datasourceConfiguration) { List endpoints = datasourceConfiguration.getEndpoints(); + SSHConnection sshProxy = datasourceConfiguration.getSshProxy(); String identifier = ""; // When hostname and port both are available, both will be used as identifier // When port is not present, default port along with hostname will be used @@ -308,6 +321,12 @@ public Mono getEndpointIdentifierForRateLimit(DatasourceConfiguration da identifier = hostName + "_" + ObjectUtils.defaultIfNull(port, DEFAULT_POSTGRES_PORT); } } + if (SSHUtils.isSSHEnabled(datasourceConfiguration, CONNECTION_METHOD_INDEX) + && sshProxy != null + && !isBlank(sshProxy.getHost())) { + identifier += "_" + sshProxy.getHost() + "_" + + SSHUtils.getSSHPortFromConfigOrDefault(datasourceConfiguration); + } return Mono.just(identifier); } @@ -700,6 +719,36 @@ public Set validateDatasource(DatasourceConfiguration datasourceConfigur invalids.add(PostgresErrorMessages.SSL_CONFIGURATION_ERROR_MSG); } + if (isSSHEnabled(datasourceConfiguration, CONNECTION_METHOD_INDEX)) { + if (datasourceConfiguration.getSshProxy() == null + || isBlank(datasourceConfiguration.getSshProxy().getHost())) { + invalids.add(DS_MISSING_SSH_HOSTNAME_ERROR_MSG); + } else { + String sshHost = datasourceConfiguration.getSshProxy().getHost(); + if (sshHost.contains("/") || sshHost.contains(":")) { + invalids.add(DS_INVALID_SSH_HOSTNAME_ERROR_MSG); + } + } + + if (StringUtils.isEmpty(datasourceConfiguration.getSshProxy().getPort())) { + invalids.add(PostgresErrorMessages.DS_MISSING_SSH_PORT_ERROR_MSG); + } + + if (isBlank(datasourceConfiguration.getSshProxy().getUsername())) { + invalids.add(DS_MISSING_SSH_USERNAME_ERROR_MSG); + } + + if (datasourceConfiguration.getSshProxy().getPrivateKey() == null + || datasourceConfiguration.getSshProxy().getPrivateKey().getKeyFile() == null + || isBlank(datasourceConfiguration + .getSshProxy() + .getPrivateKey() + .getKeyFile() + .getBase64Content())) { + invalids.add(DS_MISSING_SSH_KEY_ERROR_MSG); + } + } + return invalids; } @@ -1130,9 +1179,20 @@ private static HikariDataSource createConnectionPool( // Set up the connection URL StringBuilder urlBuilder = new StringBuilder("jdbc:postgresql://"); - List hosts = datasourceConfiguration.getEndpoints().stream() - .map(endpoint -> endpoint.getHost() + ":" + ObjectUtils.defaultIfNull(endpoint.getPort(), 5432L)) - .collect(Collectors.toList()); + List hosts = new ArrayList<>(); + + if (!isSSHEnabled(datasourceConfiguration, CONNECTION_METHOD_INDEX)) { + for (Endpoint endpoint : datasourceConfiguration.getEndpoints()) { + hosts.add(endpoint.getHost() + ":" + endpoint.getPort()); + } + } else { + ConnectionContext connectionContext; + connectionContext = getConnectionContext( + datasourceConfiguration, CONNECTION_METHOD_INDEX, DEFAULT_POSTGRES_PORT, HikariDataSource.class); + + hosts.add(LOCALHOST + ":" + + connectionContext.getSshTunnelContext().getServerSocket().getLocalPort()); + } urlBuilder.append(String.join(",", hosts)).append("/"); diff --git a/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/exceptions/PostgresErrorMessages.java b/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/exceptions/PostgresErrorMessages.java index 06af60d122c..3a88bf0fb12 100644 --- a/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/exceptions/PostgresErrorMessages.java +++ b/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/exceptions/PostgresErrorMessages.java @@ -37,6 +37,8 @@ public class PostgresErrorMessages extends BasePluginErrorMessages { public static final String DS_MISSING_HOSTNAME_ERROR_MSG = "Missing hostname."; + public static final String DS_MISSING_SSH_PORT_ERROR_MSG = "Missing SSH port."; + public static final String DS_INVALID_HOSTNAME_ERROR_MSG = "Host value cannot contain `/` or `:` characters. Found `%s`."; diff --git a/app/server/appsmith-plugins/postgresPlugin/src/main/resources/form.json b/app/server/appsmith-plugins/postgresPlugin/src/main/resources/form.json index 5c12b3c893f..6bb1ea90937 100644 --- a/app/server/appsmith-plugins/postgresPlugin/src/main/resources/form.json +++ b/app/server/appsmith-plugins/postgresPlugin/src/main/resources/form.json @@ -4,14 +4,43 @@ "sectionName": "Connection", "id": 1, "children": [ + { + "label": "Connection method", + "configProperty": "datasourceConfiguration.properties[1].key", + "initialValue": "Connection method", + "hidden": true, + "controlType": "INPUT_TEXT" + }, + { + "label": "Connection method", + "configProperty": "datasourceConfiguration.properties[1].value", + "controlType": "SEGMENTED_CONTROL", + "initialValue": "STANDARD", + "options": [ + { + "label": "Standard", + "value": "STANDARD" + }, + { + "label": "SSH tunnel", + "value": "SSH" + } + ] + }, { "label": "Connection mode", "configProperty": "datasourceConfiguration.connection.mode", "controlType": "SEGMENTED_CONTROL", "initialValue": "READ_WRITE", "options": [ - { "label": "Read / Write", "value": "READ_WRITE" }, - { "label": "Read only", "value": "READ_ONLY" } + { + "label": "Read / Write", + "value": "READ_WRITE" + }, + { + "label": "Read only", + "value": "READ_ONLY" + } ] }, { @@ -35,6 +64,32 @@ } ] }, + { + "sectionName": null, + "children": [ + { + "label": "SSH host address", + "configProperty": "datasourceConfiguration.sshProxy.endpoints[*].host", + "controlType": "KEYVALUE_ARRAY", + "validationMessage": "Please enter a valid host", + "validationRegex": "^((?![/:]).)*$", + "placeholderText": "myapp.abcde.sshHost.net" + }, + { + "label": "SSH port", + "configProperty": "datasourceConfiguration.sshProxy.endpoints[*].port", + "dataType": "NUMBER", + "controlType": "KEYVALUE_ARRAY", + "initialValue": ["22"], + "placeholderText": "22" + } + ], + "hidden": { + "path": "datasourceConfiguration.properties[1].value", + "comparison": "NOT_EQUALS", + "value": "SSH" + } + }, { "label": "Database name", "configProperty": "datasourceConfiguration.authentication.databaseName", @@ -64,6 +119,28 @@ "controlType": "INPUT_TEXT", "placeholderText": "Password", "encrypted": true + }, + { + "label": "SSH username", + "configProperty": "datasourceConfiguration.sshProxy.username", + "controlType": "INPUT_TEXT", + "placeholderText": "Username", + "hidden": { + "path": "datasourceConfiguration.properties[1].value", + "comparison": "NOT_EQUALS", + "value": "SSH" + } + }, + { + "label": "SSH key", + "configProperty": "datasourceConfiguration.sshProxy.privateKey.keyFile", + "controlType": "FILE_PICKER", + "encrypted": true, + "hidden": { + "path": "datasourceConfiguration.properties[1].value", + "comparison": "NOT_EQUALS", + "value": "SSH" + } } ] } @@ -79,13 +156,34 @@ "controlType": "DROP_DOWN", "initialValue": "DEFAULT", "options": [ - { "label": "Default", "value": "DEFAULT" }, - { "label": "Allow", "value": "ALLOW" }, - { "label": "Prefer", "value": "PREFER" }, - { "label": "Require", "value": "REQUIRE" }, - { "label": "Disable", "value": "DISABLE" }, - { "label": "Verify CA", "value": "VERIFY_CA" }, - { "label": "Verify Full", "value": "VERIFY_FULL" } + { + "label": "Default", + "value": "DEFAULT" + }, + { + "label": "Allow", + "value": "ALLOW" + }, + { + "label": "Prefer", + "value": "PREFER" + }, + { + "label": "Require", + "value": "REQUIRE" + }, + { + "label": "Disable", + "value": "DISABLE" + }, + { + "label": "Verify CA", + "value": "VERIFY_CA" + }, + { + "label": "Verify Full", + "value": "VERIFY_FULL" + } ] }, { @@ -94,8 +192,14 @@ "controlType": "DROP_DOWN", "initialValue": "-Select-", "options": [ - { "label": "Upload File", "value": "FILE" }, - { "label": "Base64 String", "value": "BASE64_STRING" } + { + "label": "Upload File", + "value": "FILE" + }, + { + "label": "Base64 String", + "value": "BASE64_STRING" + } ], "hidden": { "conditionType": "AND", @@ -114,10 +218,15 @@ } }, { - "sectionStyles": { "display": "flex", "flex-wrap": "wrap" }, + "sectionStyles": { + "display": "flex", + "flex-wrap": "wrap" + }, "children": [ { - "sectionStyles": { "marginRight": "10px" }, + "sectionStyles": { + "marginRight": "10px" + }, "children": [ { "label": "Client CA Certificate File", @@ -145,7 +254,9 @@ ] }, { - "sectionStyles": { "marginRight": "10px" }, + "sectionStyles": { + "marginRight": "10px" + }, "children": [ { "label": "Server CA Certificate File", @@ -173,7 +284,9 @@ ] }, { - "sectionStyles": { "marginRight": "10px" }, + "sectionStyles": { + "marginRight": "10px" + }, "children": [ { "label": "Client Key Certificate File", @@ -201,15 +314,21 @@ ] }, { - "sectionStyles": { "flex": 1 }, + "sectionStyles": { + "flex": 1 + }, "children": [] }, { - "sectionStyles": { "flex": 1 }, + "sectionStyles": { + "flex": 1 + }, "children": [] }, { - "sectionStyles": { "flex": 1 }, + "sectionStyles": { + "flex": 1 + }, "children": [] } ], diff --git a/app/server/appsmith-plugins/postgresPlugin/src/test/java/com/external/plugins/PostgresPluginTest.java b/app/server/appsmith-plugins/postgresPlugin/src/test/java/com/external/plugins/PostgresPluginTest.java index 7c2b084aeb8..9b66fc0e501 100644 --- a/app/server/appsmith-plugins/postgresPlugin/src/test/java/com/external/plugins/PostgresPluginTest.java +++ b/app/server/appsmith-plugins/postgresPlugin/src/test/java/com/external/plugins/PostgresPluginTest.java @@ -17,6 +17,7 @@ import com.appsmith.external.models.Property; import com.appsmith.external.models.PsParameterDTO; import com.appsmith.external.models.RequestParamDTO; +import com.appsmith.external.models.SSHConnection; import com.appsmith.external.models.SSLDetails; import com.appsmith.external.services.SharedConfig; import com.external.plugins.exceptions.PostgresErrorMessages; @@ -1719,4 +1720,111 @@ public void testGetEndpointIdentifierForRateLimit_HostPresentPortAbsent_ReturnsC }) .verifyComplete(); } + + @Test + public void testGetEndpointIdentifierForRateLimit_HostPresentPortAbsentSshEnabled_ReturnsCorrectString() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + + // Setting hostname and port + dsConfig.getEndpoints().get(0).setHost("localhost"); + dsConfig.getEndpoints().get(0).setPort(null); + + // Set ssh enabled + List properties = new ArrayList(); + properties.add(null); + properties.add(new Property("Connection Method", "SSH")); + dsConfig.setProperties(properties); + + final Mono endPointIdentifierMono = pluginExecutor.getEndpointIdentifierForRateLimit(dsConfig); + + StepVerifier.create(endPointIdentifierMono) + .assertNext(endpointIdentifier -> { + assertEquals("localhost_5432", endpointIdentifier); + }) + .verifyComplete(); + } + + @Test + public void + testGetEndpointIdentifierForRateLimit_HostPresentPortAbsentSshEnabledwithHostAndPort_ReturnsCorrectString() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + + // Setting hostname and port + dsConfig.getEndpoints().get(0).setHost("localhost"); + dsConfig.getEndpoints().get(0).setPort(null); + + // Set ssh enabled + List properties = new ArrayList(); + properties.add(null); + properties.add(new Property("Connection Method", "SSH")); + dsConfig.setProperties(properties); + + SSHConnection sshProxy = new SSHConnection(); + sshProxy.setHost("sshHost"); + sshProxy.setPort(22L); + dsConfig.setSshProxy(sshProxy); + + final Mono endPointIdentifierMono = pluginExecutor.getEndpointIdentifierForRateLimit(dsConfig); + + StepVerifier.create(endPointIdentifierMono) + .assertNext(endpointIdentifier -> { + assertEquals("localhost_5432_sshHost_22", endpointIdentifier); + }) + .verifyComplete(); + } + + @Test + public void + testGetEndpointIdentifierForRateLimit_HostPresentPortAbsentSshEnabledwithHostAndNullPort_ReturnsCorrectString() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + + // Setting hostname and port + dsConfig.getEndpoints().get(0).setHost("localhost"); + dsConfig.getEndpoints().get(0).setPort(null); + + // Set ssh enabled + List properties = new ArrayList(); + properties.add(null); + properties.add(new Property("Connection Method", "SSH")); + dsConfig.setProperties(properties); + + SSHConnection sshProxy = new SSHConnection(); + sshProxy.setHost("sshHost"); + dsConfig.setSshProxy(sshProxy); + + final Mono endPointIdentifierMono = pluginExecutor.getEndpointIdentifierForRateLimit(dsConfig); + + StepVerifier.create(endPointIdentifierMono) + .assertNext(endpointIdentifier -> { + assertEquals("localhost_5432_sshHost_22", endpointIdentifier); + }) + .verifyComplete(); + } + + @Test + public void + testGetEndpointIdentifierForRateLimit_EndpointAbsentSshEnabledwithHostAndNullPort_ReturnsCorrectString() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + + // Setting hostname and port + dsConfig.setEndpoints(new ArrayList()); + + // Set ssh enabled + List properties = new ArrayList(); + properties.add(null); + properties.add(new Property("Connection Method", "SSH")); + dsConfig.setProperties(properties); + + SSHConnection sshProxy = new SSHConnection(); + sshProxy.setHost("sshHost"); + dsConfig.setSshProxy(sshProxy); + + final Mono endPointIdentifierMono = pluginExecutor.getEndpointIdentifierForRateLimit(dsConfig); + + StepVerifier.create(endPointIdentifierMono) + .assertNext(endpointIdentifier -> { + assertEquals("_sshHost_22", endpointIdentifier); + }) + .verifyComplete(); + } }