Added sslmode/sslrootcert for postgresql and cockroachdb (#66)

This commit is contained in:
Marco Yeung
2022-04-27 13:47:29 -04:00
committed by GitHub
parent dca042dab0
commit 624fed286c
2 changed files with 254 additions and 2 deletions

View File

@@ -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<String, Object> 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
* <a href="https://www.postgresql.org/docs/14/libpq-connect.html">PostgreSQL Doc</a>.
* <p>
* CockroachDB, which shares the same 'postgresql://' protocol as PostgreSQL, has customized options to meet its
* distributed database nature.
* Refer to <a href="https://www.cockroachlabs.com/docs/v21.2/connection-parameters#additional-connection-parameters">Client Connection Parameters</a>.
*/
private String buildDbOptions(Binding binding) {
String options = binding.getSecret().getOrDefault(OPTIONS, "");
String crdbOption = "";
List<String> 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 <a href="https://www.postgresql.org/docs/14/libpq-connect.html">PostgreSQL Doc</a>
*/
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();
}
}

View File

@@ -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");
}
}