From 624fed286ca25a3bd68770cfc174ce7a230c4093 Mon Sep 17 00:00:00 2001 From: Marco Yeung Date: Wed, 27 Apr 2022 13:47:29 -0400 Subject: [PATCH] Added sslmode/sslrootcert for postgresql and cockroachdb (#66) --- ...PostgreSqlBindingsPropertiesProcessor.java | 102 +++++++++++- ...greSqlBindingsPropertiesProcessorTest.java | 154 ++++++++++++++++++ 2 files changed, 254 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/springframework/cloud/bindings/boot/PostgreSqlBindingsPropertiesProcessor.java b/src/main/java/org/springframework/cloud/bindings/boot/PostgreSqlBindingsPropertiesProcessor.java index e098263..b5dbe72 100644 --- a/src/main/java/org/springframework/cloud/bindings/boot/PostgreSqlBindingsPropertiesProcessor.java +++ b/src/main/java/org/springframework/cloud/bindings/boot/PostgreSqlBindingsPropertiesProcessor.java @@ -20,6 +20,9 @@ import org.springframework.cloud.bindings.Binding; import org.springframework.cloud.bindings.Bindings; import org.springframework.core.env.Environment; +import java.nio.file.FileSystems; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import static org.springframework.cloud.bindings.boot.Guards.isTypeEnabled; @@ -35,6 +38,21 @@ public final class PostgreSqlBindingsPropertiesProcessor implements BindingsProp * The {@link Binding} type that this processor is interested in: {@value}. **/ public static final String TYPE = "postgresql"; + /** + * sslmode determines whether or with what priority a secure SSL TCP/IP connection will be negotiated with the server. + */ + public static final String SSL_MODE = "sslmode"; + /** + * sslrootcert specifies the name of a file containing SSL certificate authority (CA) certificate(s). + */ + public static final String SSL_ROOT_CERT = "sslrootcert"; + /** + * options Specifies command-line options to send to the server at connection start. + * CockroachDB uses this to pass in cluster routing id + */ + public static final String OPTIONS = "options"; + public static final String SPRING_DATASOURCE_URL = "spring.datasource.url"; + public static final String SPRING_R2DBC_URL = "spring.r2dbc.url"; @Override public void process(Environment environment, Bindings bindings, Map properties) { @@ -47,8 +65,21 @@ public final class PostgreSqlBindingsPropertiesProcessor implements BindingsProp //jdbc properties map.from("password").to("spring.datasource.password"); - map.from("host", "port", "database").to("spring.datasource.url", + map.from("host", "port", "database").to(SPRING_DATASOURCE_URL, (host, port, database) -> String.format("jdbc:postgresql://%s:%s/%s", host, port, database)); + + String sslParam = buildSslModeParam(binding); + String dbOptions = buildDbOptions(binding); + String sslModeOptions = dbOptions; + if (!"".equals(sslParam) && !"".equals(sslModeOptions)) { + sslModeOptions = sslParam + "&" + sslModeOptions; + } else if (!"".equals(sslParam) ) { + sslModeOptions = sslParam; + } + + if (!"".equals(sslModeOptions)) { + properties.put(SPRING_DATASOURCE_URL, properties.get(SPRING_DATASOURCE_URL) + "?" + sslModeOptions); + } map.from("username").to("spring.datasource.username"); // jdbcURL takes precedence @@ -58,8 +89,11 @@ public final class PostgreSqlBindingsPropertiesProcessor implements BindingsProp //r2dbc properties map.from("password").to("spring.r2dbc.password"); - map.from("host", "port", "database").to("spring.r2dbc.url", + map.from("host", "port", "database").to(SPRING_R2DBC_URL, (host, port, database) -> String.format("r2dbc:postgresql://%s:%s/%s", host, port, database)); + if (!"".equals(sslModeOptions)) { + properties.put(SPRING_R2DBC_URL, properties.get(SPRING_R2DBC_URL) + "?" + sslModeOptions); + } map.from("username").to("spring.r2dbc.username"); // r2dbcURL takes precedence @@ -67,4 +101,68 @@ public final class PostgreSqlBindingsPropertiesProcessor implements BindingsProp }); } + /** + * Returns a concatenated list of options parameters defined in the bound file `options` in the format specified in + * PostgreSQL Doc. + *

+ * CockroachDB, which shares the same 'postgresql://' protocol as PostgreSQL, has customized options to meet its + * distributed database nature. + * Refer to Client Connection Parameters. + */ + private String buildDbOptions(Binding binding) { + String options = binding.getSecret().getOrDefault(OPTIONS, ""); + String crdbOption = ""; + List dbOptions = new ArrayList<>(); + if (!options.equals("")) { + String[] allOpts = options.split("&"); + for (String o : allOpts) { + String[] keyval = o.split("="); + if (keyval.length != 2 || keyval[0].length() == 0 || keyval[1].length() == 0) { + continue; + } + if (keyval[0].equals("--cluster")) { + crdbOption = keyval[0] + "=" + keyval[1]; + } else { + dbOptions.add("-c " + keyval[0] + "=" + keyval[1]); + } + } + } + String combinedOptions = crdbOption; + if (dbOptions.size() > 0) { + String otherOpts = String.join(" ", dbOptions); + if (!combinedOptions.equals("")) { + combinedOptions = combinedOptions + " " + otherOpts; + } else { + combinedOptions = otherOpts; + } + } + if (!"".equals(combinedOptions)) { + combinedOptions = "options=" + combinedOptions; + } + return combinedOptions; + } + + /** + * Returns a concatenated string of all ssl parameters for enabling one-way TLS (PostgreSQL certifies itself) + * Refer to PostgreSQL Doc + */ + private String buildSslModeParam(Binding binding) { + //process ssl params + //https://www.postgresql.org/docs/14/libpq-connect.html + String sslmode = binding.getSecret().getOrDefault(SSL_MODE, ""); + String sslRootCert = binding.getSecret().getOrDefault(SSL_ROOT_CERT, ""); + StringBuilder sslparam = new StringBuilder(); + if (!"".equals(sslmode)) { + sslparam.append(SSL_MODE).append("=").append(sslmode); + } + if (!"".equals(sslRootCert)) { + if (!"".equals(sslmode)) { + sslparam.append("&"); + } + sslparam.append(SSL_ROOT_CERT).append("=") + .append(binding.getPath()).append(FileSystems.getDefault().getSeparator()) + .append(sslRootCert); + } + return sslparam.toString(); + } } diff --git a/src/test/java/org/springframework/cloud/bindings/boot/PostgreSqlBindingsPropertiesProcessorTest.java b/src/test/java/org/springframework/cloud/bindings/boot/PostgreSqlBindingsPropertiesProcessorTest.java index 80e010d..f083fbb 100644 --- a/src/test/java/org/springframework/cloud/bindings/boot/PostgreSqlBindingsPropertiesProcessorTest.java +++ b/src/test/java/org/springframework/cloud/bindings/boot/PostgreSqlBindingsPropertiesProcessorTest.java @@ -111,4 +111,158 @@ final class PostgreSqlBindingsPropertiesProcessorTest { assertThat(properties).isEmpty(); } + private final FluentMap secretSsl = new FluentMap() + .withEntry(Binding.TYPE, TYPE) + .withEntry("database", "test-database") + .withEntry("host", "test-host") + .withEntry("password", "test-password") + .withEntry("port", "test-port") + .withEntry("username", "test-username") + .withEntry("sslmode", "verify-full") + .withEntry("sslrootcert", "root.cert") + .withEntry("options", "--cluster=routing-id&opt=val1"); + + @Test + @DisplayName("composes jdbc url from host port and database with sslmode and crdb option") + void testJdbcWithSsl() { + Bindings bindings = new Bindings( + new Binding("test-name", Paths.get("bindings"), secretSsl) + ); + new PostgreSqlBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties) + .containsEntry("spring.datasource.driver-class-name", "org.postgresql.Driver") + .containsEntry("spring.datasource.password", "test-password") + .containsEntry("spring.datasource.url", "jdbc:postgresql://test-host:test-port/test-database?sslmode=verify-full&sslrootcert=bindings/root.cert&options=--cluster=routing-id -c opt=val1") + .containsEntry("spring.datasource.username", "test-username"); + } + + private final FluentMap secretInvalidCrdbOption = new FluentMap() + .withEntry(Binding.TYPE, TYPE) + .withEntry("database", "test-database") + .withEntry("host", "test-host") + .withEntry("password", "test-password") + .withEntry("port", "test-port") + .withEntry("username", "test-username") + .withEntry("sslmode", "verify-full") + .withEntry("sslrootcert", "root.cert") + .withEntry("options", "-cluster=routing-id&opt=val1"); + + @Test + @DisplayName("composes jdbc url from host port and database with sslmode and crdb option") + void testJdbcWithInvalidCrdbOption() { + Bindings bindings = new Bindings( + new Binding("test-name", Paths.get("bindings"), secretInvalidCrdbOption) + ); + new PostgreSqlBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties) + .containsEntry("spring.datasource.driver-class-name", "org.postgresql.Driver") + .containsEntry("spring.datasource.password", "test-password") + .containsEntry("spring.datasource.url", "jdbc:postgresql://test-host:test-port/test-database?sslmode=verify-full&sslrootcert=bindings/root.cert&options=-c -cluster=routing-id -c opt=val1") + .containsEntry("spring.datasource.username", "test-username"); + } + + @Test + @DisplayName("composes r2dbc url from host port and database with sslmode and crdb option") + void testR2dbcWithSsl() { + Bindings bindings = new Bindings( + new Binding("test-name", Paths.get("bindings"), secretSsl) + ); + new PostgreSqlBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties) + .containsEntry("spring.r2dbc.password", "test-password") + .containsEntry("spring.r2dbc.url", "r2dbc:postgresql://test-host:test-port/test-database?sslmode=verify-full&sslrootcert=bindings/root.cert&options=--cluster=routing-id -c opt=val1") + .containsEntry("spring.r2dbc.username", "test-username"); + } + + private final FluentMap secretSslDisable = new FluentMap() + .withEntry(Binding.TYPE, TYPE) + .withEntry("database", "test-database") + .withEntry("host", "test-host") + .withEntry("password", "test-password") + .withEntry("port", "test-port") + .withEntry("username", "test-username") + .withEntry("sslmode", "disable"); + + @Test + @DisplayName("composes jdbc url from host port and database with sslmode disable") + void testJdbcWithSslDisable() { + Bindings bindings = new Bindings( + new Binding("test-name", Paths.get("bindings"), secretSslDisable) + ); + new PostgreSqlBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties) + .containsEntry("spring.datasource.driver-class-name", "org.postgresql.Driver") + .containsEntry("spring.datasource.password", "test-password") + .containsEntry("spring.datasource.url", "jdbc:postgresql://test-host:test-port/test-database?sslmode=disable") + .containsEntry("spring.datasource.username", "test-username"); + } + + private final FluentMap secretWithDBoptions = new FluentMap() + .withEntry(Binding.TYPE, TYPE) + .withEntry("database", "test-database") + .withEntry("host", "test-host") + .withEntry("password", "test-password") + .withEntry("port", "test-port") + .withEntry("username", "test-username") + .withEntry("options", "opt1=val1&opt2=val2"); + + @Test + @DisplayName("composes jdbc url from host port and database with DB options") + void testJdbcWithDBoptions() { + Bindings bindings = new Bindings( + new Binding("test-name", Paths.get("bindings"), secretWithDBoptions) + ); + new PostgreSqlBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties) + .containsEntry("spring.datasource.driver-class-name", "org.postgresql.Driver") + .containsEntry("spring.datasource.password", "test-password") + .containsEntry("spring.datasource.url", "jdbc:postgresql://test-host:test-port/test-database?options=-c opt1=val1 -c opt2=val2") + .containsEntry("spring.datasource.username", "test-username"); + } + + private final FluentMap secretWithInvalidDBOptions = new FluentMap() + .withEntry(Binding.TYPE, TYPE) + .withEntry("database", "test-database") + .withEntry("host", "test-host") + .withEntry("password", "test-password") + .withEntry("port", "test-port") + .withEntry("username", "test-username") + .withEntry("options", "opt1=val1&opt"); + + @Test + @DisplayName("composes jdbc url from host port and database with invalid DB options") + void testJdbcWithInvaildDBoptions() { + Bindings bindings = new Bindings( + new Binding("test-name", Paths.get("bindings"), secretWithInvalidDBOptions) + ); + new PostgreSqlBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties) + .containsEntry("spring.datasource.driver-class-name", "org.postgresql.Driver") + .containsEntry("spring.datasource.password", "test-password") + .containsEntry("spring.datasource.url", "jdbc:postgresql://test-host:test-port/test-database?options=-c opt1=val1") + .containsEntry("spring.datasource.username", "test-username"); + } + + private final FluentMap secretWithEmptyDBOptions = new FluentMap() + .withEntry(Binding.TYPE, TYPE) + .withEntry("database", "test-database") + .withEntry("host", "test-host") + .withEntry("password", "test-password") + .withEntry("port", "test-port") + .withEntry("username", "test-username") + .withEntry("options", ""); + + @Test + @DisplayName("composes jdbc url from host port and database with empty DB options") + void testJdbcWithEmptyDBoptions() { + Bindings bindings = new Bindings( + new Binding("test-name", Paths.get("bindings"), secretWithEmptyDBOptions) + ); + new PostgreSqlBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties) + .containsEntry("spring.datasource.driver-class-name", "org.postgresql.Driver") + .containsEntry("spring.datasource.password", "test-password") + .containsEntry("spring.datasource.url", "jdbc:postgresql://test-host:test-port/test-database") + .containsEntry("spring.datasource.username", "test-username"); + } }