Skip to content

Commit 69d5bc8

Browse files
committed
Support authentication via HashiCorp Vault
1 parent 9f72201 commit 69d5bc8

File tree

14 files changed

+219
-41
lines changed

14 files changed

+219
-41
lines changed

pgjdbc/pom.xml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<artifactId>postgresql</artifactId>
1111
<packaging>bundle</packaging>
1212
<name>PostgreSQL JDBC Driver - JDBC 4.2</name>
13-
<version>42.1.2-SNAPSHOT</version>
13+
<version>42.1.2-VAULT</version>
1414
<description>Java JDBC 4.2 (JRE 8+) driver for PostgreSQL database</description>
1515
<url>https://github.com/pgjdbc/pgjdbc</url>
1616

@@ -35,7 +35,13 @@
3535
<skip.assembly>false</skip.assembly>
3636
<checkstyle.version>7.4</checkstyle.version>
3737
</properties>
38-
38+
<dependencies>
39+
<dependency>
40+
<groupId>com.bettercloud</groupId>
41+
<artifactId>vault-java-driver</artifactId>
42+
<version>3.0.0-SNAPSHOT</version>
43+
</dependency>
44+
</dependencies>
3945
<profiles>
4046
<profile>
4147
<id>translate</id>

pgjdbc/src/main/java/org/postgresql/Driver.java

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import org.postgresql.util.PSQLState;
1515
import org.postgresql.util.SharedTimer;
1616
import org.postgresql.util.WriterHandler;
17+
import org.postgresql.util.VaultLookup;
18+
import org.postgresql.util.VaultRenewalTaskScheduler;
1719

1820
import java.io.IOException;
1921
import java.io.InputStream;
@@ -190,7 +192,7 @@ private Properties loadDefaultProperties() throws IOException {
190192
* Our protocol takes the forms:
191193
*
192194
* <PRE>
193-
* jdbc:postgresql://host:port/database?param1=val1&amp;...
195+
* jdbc:postgresqlvault://host:port/database?param1=val1&amp;...
194196
* </PRE>
195197
*
196198
* @param url the URL of the database to connect to
@@ -202,8 +204,9 @@ private Properties loadDefaultProperties() throws IOException {
202204
public java.sql.Connection connect(String url, Properties info) throws SQLException {
203205
// get defaults
204206
Properties defaults;
207+
Connection conn;
205208

206-
if (!url.startsWith("jdbc:postgresql:")) {
209+
if (!url.startsWith("jdbc:postgresqlvault:")) {
207210
return null;
208211
}
209212
try {
@@ -234,10 +237,28 @@ public java.sql.Connection connect(String url, Properties info) throws SQLExcept
234237
return null;
235238
}
236239
try {
240+
final VaultLookup vault;
237241
// Setup java.util.logging.Logger using connection properties.
238242
setupLoggerFromProperties(props);
243+
244+
if (props.getProperty("vaultHost") != null) {
245+
LOGGER.log(Level.INFO, "Attempting to authenticate with Vault server at {0}", props.getProperty("vaultHost"));
246+
try {
247+
vault = new VaultLookup(props.getProperty("vaultHost"), props.getProperty("vaultAuthPath", "userpass"), props.getProperty("vaultDBPath", "database/creds/default"), props.getProperty("user"), props.getProperty("password"));
248+
props.setProperty("user", vault.getUser());
249+
props.setProperty("password", vault.getPass());
250+
} catch (Exception e) {
251+
LOGGER.log(Level.SEVERE, "Vault exception: {0}", e.getMessage());
252+
return null;
253+
}
254+
} else {
255+
// Otherwise the Java compiler won't realize the if block above is identical
256+
// to the guard before creating the renewal task, and think the variable isn't
257+
// initialized
258+
vault = null;
259+
}
239260

240-
LOGGER.log(Level.FINE, "Connecting with URL: {0}", url);
261+
LOGGER.log(Level.INFO, "Connecting with URL: {0}", url);
241262

242263
// Enforce login timeout, if specified, by running the connection
243264
// attempt in a separate thread. If we hit the timeout without the
@@ -249,14 +270,18 @@ public java.sql.Connection connect(String url, Properties info) throws SQLExcept
249270
// more details.
250271
long timeout = timeout(props);
251272
if (timeout <= 0) {
252-
return makeConnection(url, props);
273+
conn = makeConnection(url, props);
274+
} else {
275+
ConnectThread ct = new ConnectThread(url, props);
276+
Thread thread = new Thread(ct, "PostgreSQL JDBC driver connection thread");
277+
thread.setDaemon(true); // Don't prevent the VM from shutting down
278+
thread.start();
279+
conn = ct.getResult(timeout);
253280
}
254-
255-
ConnectThread ct = new ConnectThread(url, props);
256-
Thread thread = new Thread(ct, "PostgreSQL JDBC driver connection thread");
257-
thread.setDaemon(true); // Don't prevent the VM from shutting down
258-
thread.start();
259-
return ct.getResult(timeout);
281+
if (props.getProperty("vaultHost") != null) {
282+
final VaultRenewalTaskScheduler renewalThread = new VaultRenewalTaskScheduler((PgConnection)conn, vault.getRenewalConfig());
283+
}
284+
return conn;
260285
} catch (PSQLException ex1) {
261286
LOGGER.log(Level.SEVERE, "Connection error: ", ex1);
262287
// re-throw the exception, otherwise it will be caught next, and a
@@ -453,7 +478,7 @@ private static Connection makeConnection(String url, Properties props) throws SQ
453478
/**
454479
* Returns true if the driver thinks it can open a connection to the given URL. Typically, drivers
455480
* will return true if they understand the subprotocol specified in the URL and false if they
456-
* don't. Our protocols start with jdbc:postgresql:
481+
* don't. Our protocols start with jdbc:postgresqlvault:
457482
*
458483
* @param url the URL of the driver
459484
* @return true if this driver accepts the given URL
@@ -534,10 +559,10 @@ public static Properties parseURL(String url, Properties defaults) {
534559
l_urlArgs = url.substring(l_qPos + 1);
535560
}
536561

537-
if (!l_urlServer.startsWith("jdbc:postgresql:")) {
562+
if (!l_urlServer.startsWith("jdbc:postgresqlvault:")) {
538563
return null;
539564
}
540-
l_urlServer = l_urlServer.substring("jdbc:postgresql:".length());
565+
l_urlServer = l_urlServer.substring("jdbc:postgresqlvault:".length());
541566

542567
if (l_urlServer.startsWith("//")) {
543568
l_urlServer = l_urlServer.substring(2);

pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1048,7 +1048,7 @@ public void setLoggerFile(String loggerFile) {
10481048
*/
10491049
public String getUrl() {
10501050
StringBuilder url = new StringBuilder(100);
1051-
url.append("jdbc:postgresql://");
1051+
url.append("jdbc:postgresqlvault://");
10521052
url.append(serverName);
10531053
if (portNumber != 0) {
10541054
url.append(":").append(portNumber);

pgjdbc/src/main/java/org/postgresql/util/PGJDBCMain.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public static void main(String[] args) {
1818
System.out.printf("The PgJDBC driver is not an executable Java program.%n%n"
1919
+ "You must install it according to the JDBC driver installation "
2020
+ "instructions for your application / container / appserver, "
21-
+ "then use it by specifying a JDBC URL of the form %n jdbc:postgresql://%n"
21+
+ "then use it by specifying a JDBC URL of the form %n jdbc:postgresqlvault://%n"
2222
+ "or using an application specific method.%n%n"
2323
+ "See the PgJDBC documentation: http://jdbc.postgresql.org/documentation/head/index.html%n%n"
2424
+ "This command has had no effect.%n");
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package org.postgresql.util;
2+
3+
import java.util.Map;
4+
import java.util.HashMap;
5+
import com.bettercloud.vault.Vault;
6+
import com.bettercloud.vault.VaultConfig;
7+
import com.bettercloud.vault.VaultException;
8+
import com.bettercloud.vault.response.LogicalResponse;
9+
10+
11+
public class VaultLookup {
12+
private final String user;
13+
private final String pass;
14+
private final RenewalConfig renewConfig;
15+
16+
public static final class RenewalConfig {
17+
public final VaultConfig vltConfig;
18+
public final String leaseId;
19+
public final Number initialTTL;
20+
21+
private RenewalConfig (VaultConfig vltConfig, String leaseId, Number initialTTL) {
22+
this.vltConfig = vltConfig;
23+
this.leaseId = leaseId;
24+
this.initialTTL = initialTTL;
25+
}
26+
}
27+
28+
public VaultLookup(String VaultAddr, String VaultAuthPath, String VaultDBPath, String VaultUser, String VaultPass) throws VaultException {
29+
final VaultConfig vltConfig = new VaultConfig(VaultAddr);
30+
final Vault vault = new Vault(vltConfig);
31+
Number ttl = 1;
32+
String token;
33+
34+
token = vault.auth().loginByUserPass(VaultAuthPath, VaultUser, VaultPass).getAuthClientToken();
35+
vltConfig.token(token);
36+
try {
37+
final LogicalResponse res = vault.logical().read(VaultDBPath);
38+
user = res.getData().get("username");
39+
pass = res.getData().get("password");
40+
ttl = res.getLeaseDuration();
41+
renewConfig = new RenewalConfig(vltConfig, res.getLeaseId(), ttl);
42+
} finally {
43+
// Don't logout immediately, as it would revoke the DB credentials before we login.
44+
// Instead logout after the db creds expire (or in 1 second if we didn't get creds)
45+
// This won't work in case the DB driver explicitly disconnects the user when the credentials are revoked.
46+
// In that case, it would be better to create a token role which can create an orphaned token which can create the database credentials
47+
final Map<String, Object> params = new HashMap();
48+
params.put("increment", ttl);
49+
vault.logical().write("auth/token/renew-self", params);
50+
}
51+
52+
}
53+
54+
public String getUser() {
55+
return user;
56+
}
57+
public String getPass() {
58+
return pass;
59+
}
60+
public RenewalConfig getRenewalConfig() {
61+
return renewConfig;
62+
}
63+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package org.postgresql.util;
2+
3+
import java.util.Map;
4+
import java.util.HashMap;
5+
import java.util.Timer;
6+
import java.util.TimerTask;
7+
import java.util.logging.Level;
8+
import java.util.logging.Logger;
9+
import org.postgresql.util.SharedTimer;
10+
import org.postgresql.util.VaultLookup;
11+
import org.postgresql.jdbc.PgConnection;
12+
import com.bettercloud.vault.Vault;
13+
import com.bettercloud.vault.VaultConfig;
14+
import com.bettercloud.vault.response.LogicalResponse;
15+
16+
// The renewal task latches onto an existing thread scheduler on the connection object
17+
// Closing the connection will automatically purge the scheduler and stop renewing the lease
18+
19+
public class VaultRenewalTaskScheduler {
20+
private final SharedTimer timer;
21+
private static final Logger LOGGER = Logger.getLogger(VaultRenewalTask.class.getName());
22+
23+
private class RenewalTask extends TimerTask {
24+
private final String leaseId;
25+
private final PgConnection conn;
26+
private final VaultConfig vltConfig;
27+
public RenewalTask(PgConnection conn, VaultLookup.RenewalConfig renewConfig) {
28+
this.conn = conn;
29+
this.vltConfig = renewConfig.vltConfig;
30+
this.leaseId = renewConfig.leaseId;
31+
}
32+
33+
/**
34+
* Renew the lease and token with Vault and reschedule
35+
*/
36+
@Override
37+
public void run() {
38+
final Vault vault = new Vault(vltConfig);
39+
try {
40+
LOGGER.log(Level.FINE, "Attempting to renew credentials for {0}", leaseId);
41+
// First, renew the database lease according to the default TTL
42+
final Map<String, Object> params = new HashMap();
43+
params.put("lease_id", leaseId);
44+
final LogicalResponse res = vault.logical().write("sys/renew", params);
45+
Number ttl = res.getLeaseDuration();
46+
// Now renew the token to the same TTL as the database
47+
params.clear();
48+
params.put("increment", ttl);
49+
vault.logical().write("auth/token/renew-self", params);
50+
} catch (Exception e) {
51+
LOGGER.log(Level.SEVERE, e.getMessage().toString());
52+
// Retries are caught and dealt with by the Vault driver, so anything else is fatal.
53+
// Attempt to close the connection.
54+
try {
55+
conn.close();
56+
VaultRenewalTaskScheduler.this.shutdown();
57+
} catch (Exception e2) {}
58+
}
59+
}
60+
}
61+
62+
public VaultRenewalTaskScheduler(PgConnection conn, VaultLookup.RenewalConfig renewConfig) {
63+
final RenewalTask task = new RenewalTask(conn, renewConfig);
64+
this.timer = new SharedTimer();
65+
final long nextInterval = this.interval(renewConfig.initialTTL);
66+
timer.getTimer().schedule(task, nextInterval, nextInterval);
67+
LOGGER.log(Level.INFO, "Scheduling vault renewal every {0}ms", String.valueOf(nextInterval));
68+
}
69+
70+
71+
public void shutdown() {
72+
LOGGER.log(Level.FINER, "Shut down timer");
73+
timer.releaseTimer();
74+
}
75+
76+
/**
77+
* Return the scheduling interval. Defautls to 2/3 of the lease ttl
78+
*
79+
* @param ttl Lease ttl
80+
*/
81+
public long interval(Number ttl) {
82+
return (long)((Long)ttl * 1000 * (2.0f / 3));
83+
}
84+
}

pgjdbc/src/test/java/org/postgresql/test/TestUtil.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public static String getURL(String server, int port) {
7373
ssl = "&ssl=" + getSSL();
7474
}
7575

76-
return "jdbc:postgresql://"
76+
return "jdbc:postgresqlvault://"
7777
+ server + ":"
7878
+ port + "/"
7979
+ getDatabase()

pgjdbc/src/test/java/org/postgresql/test/hostchooser/MultiHostsConnectionTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ private static Connection getConnection(HostRequirement hostType, boolean reset,
101101
}
102102

103103
StringBuilder sb = new StringBuilder();
104-
sb.append("jdbc:postgresql://");
104+
sb.append("jdbc:postgresqlvault://");
105105
for (String target : targets) {
106106
sb.append(target).append(',');
107107
}

pgjdbc/src/test/java/org/postgresql/test/jdbc2/ConnectTimeoutTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
public class ConnectTimeoutTest {
2323
// The IP below is non-routable (see http://stackoverflow.com/a/904609/1261287)
2424
private static final String UNREACHABLE_HOST = "10.255.255.1";
25-
private static final String UNREACHABLE_URL = "jdbc:postgresql://" + UNREACHABLE_HOST + ":5432/test";
25+
private static final String UNREACHABLE_URL = "jdbc:postgresqlvault://" + UNREACHABLE_HOST + ":5432/test";
2626
private static final int CONNECT_TIMEOUT = 5;
2727

2828
@Before

pgjdbc/src/test/java/org/postgresql/test/jdbc2/DriverTest.java

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,26 +46,26 @@ public void testAcceptsURL() throws Exception {
4646
assertNotNull(drv);
4747

4848
// These are always correct
49-
verifyUrl(drv, "jdbc:postgresql:test", "localhost", "5432", "test");
50-
verifyUrl(drv, "jdbc:postgresql://localhost/test", "localhost", "5432", "test");
51-
verifyUrl(drv, "jdbc:postgresql://localhost:5432/test", "localhost", "5432", "test");
52-
verifyUrl(drv, "jdbc:postgresql://127.0.0.1/anydbname", "127.0.0.1", "5432", "anydbname");
53-
verifyUrl(drv, "jdbc:postgresql://127.0.0.1:5433/hidden", "127.0.0.1", "5433", "hidden");
54-
verifyUrl(drv, "jdbc:postgresql://[::1]:5740/db", "[::1]", "5740", "db");
49+
verifyUrl(drv, "jdbc:postgresqlvault:test", "localhost", "5432", "test");
50+
verifyUrl(drv, "jdbc:postgresqlvault://localhost/test", "localhost", "5432", "test");
51+
verifyUrl(drv, "jdbc:postgresqlvault://localhost:5432/test", "localhost", "5432", "test");
52+
verifyUrl(drv, "jdbc:postgresqlvault://127.0.0.1/anydbname", "127.0.0.1", "5432", "anydbname");
53+
verifyUrl(drv, "jdbc:postgresqlvault://127.0.0.1:5433/hidden", "127.0.0.1", "5433", "hidden");
54+
verifyUrl(drv, "jdbc:postgresqlvault://[::1]:5740/db", "[::1]", "5740", "db");
5555

5656
// Badly formatted url's
5757
assertTrue(!drv.acceptsURL("jdbc:postgres:test"));
5858
assertTrue(!drv.acceptsURL("postgresql:test"));
5959
assertTrue(!drv.acceptsURL("db"));
60-
assertTrue(!drv.acceptsURL("jdbc:postgresql://localhost:5432a/test"));
60+
assertTrue(!drv.acceptsURL("jdbc:postgresqlvault://localhost:5432a/test"));
6161

6262
// failover urls
63-
verifyUrl(drv, "jdbc:postgresql://localhost,127.0.0.1:5432/test", "localhost,127.0.0.1",
63+
verifyUrl(drv, "jdbc:postgresqlvault://localhost,127.0.0.1:5432/test", "localhost,127.0.0.1",
6464
"5432,5432", "test");
65-
verifyUrl(drv, "jdbc:postgresql://localhost:5433,127.0.0.1:5432/test", "localhost,127.0.0.1",
65+
verifyUrl(drv, "jdbc:postgresqlvault://localhost:5433,127.0.0.1:5432/test", "localhost,127.0.0.1",
6666
"5433,5432", "test");
67-
verifyUrl(drv, "jdbc:postgresql://[::1],[::1]:5432/db", "[::1],[::1]", "5432,5432", "db");
68-
verifyUrl(drv, "jdbc:postgresql://[::1]:5740,127.0.0.1:5432/db", "[::1],127.0.0.1", "5740,5432",
67+
verifyUrl(drv, "jdbc:postgresqlvault://[::1],[::1]:5432/db", "[::1],[::1]", "5432,5432", "db");
68+
verifyUrl(drv, "jdbc:postgresqlvault://[::1]:5740,127.0.0.1:5432/db", "[::1],127.0.0.1", "5740,5432",
6969
"db");
7070
}
7171

@@ -111,7 +111,7 @@ public void testConnect() throws Exception {
111111
*/
112112
@Test
113113
public void testConnectFailover() throws Exception {
114-
String url = "jdbc:postgresql://invalidhost.not.here," + TestUtil.getServer() + ":"
114+
String url = "jdbc:postgresqlvault://invalidhost.not.here," + TestUtil.getServer() + ":"
115115
+ TestUtil.getPort() + "/" + TestUtil.getDatabase() + "?connectTimeout=5";
116116
Connection con = DriverManager.getConnection(url, TestUtil.getUser(), TestUtil.getPassword());
117117
assertNotNull(con);

pgjdbc/src/test/java/org/postgresql/test/jdbc2/LoginTimeoutTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ public void testTimeoutOccurs() throws Exception {
139139
new Thread(helper, "timeout listen helper").start();
140140

141141
try {
142-
String url = "jdbc:postgresql://" + helper.getHost() + ":" + helper.getPort() + "/dummy";
142+
String url = "jdbc:postgresqlvault://" + helper.getHost() + ":" + helper.getPort() + "/dummy";
143143
Properties props = new Properties();
144144
props.setProperty("user", "dummy");
145145
props.setProperty("loginTimeout", "5");

0 commit comments

Comments
 (0)