Skip to content

Commit d5bc557

Browse files
authored
Merge pull request #76 from ebean-orm/feature/add-password2
Add support for password2 and transparent password rotation
2 parents ef24877 + 20eccf5 commit d5bc557

6 files changed

Lines changed: 176 additions & 29 deletions

File tree

ebean-datasource-api/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
<dependency>
2323
<groupId>io.avaje</groupId>
2424
<artifactId>junit</artifactId>
25-
<version>1.1</version>
25+
<version>1.3</version>
2626
<scope>test</scope>
2727
</dependency>
2828

ebean-datasource-api/src/main/java/io/ebean/datasource/DataSourceConfig.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public class DataSourceConfig {
2929
private String url;
3030
private String username;
3131
private String password;
32+
private String password2;
3233
private String schema;
3334
private String driver;
3435
private InitDatabase initDatabase;
@@ -81,6 +82,7 @@ public DataSourceConfig copy() {
8182
copy.readOnlyUrl = readOnlyUrl;
8283
copy.username = username;
8384
copy.password = password;
85+
copy.password2 = password2;
8486
copy.schema = schema;
8587
copy.platform = platform;
8688
copy.ownerUsername = ownerUsername;
@@ -136,6 +138,9 @@ public DataSourceConfig setDefaults(DataSourceConfig other) {
136138
if (password == null) {
137139
password = other.password;
138140
}
141+
if (password2 == null) {
142+
password2 = other.password2;
143+
}
139144
if (schema == null) {
140145
schema = other.schema;
141146
}
@@ -254,6 +259,21 @@ public DataSourceConfig setPassword(String password) {
254259
return this;
255260
}
256261

262+
/**
263+
* Return the database alternate password2.
264+
*/
265+
public String getPassword2() {
266+
return password2;
267+
}
268+
269+
/**
270+
* Set the database alternate password2.
271+
*/
272+
public DataSourceConfig setPassword2(String password2) {
273+
this.password2 = password2;
274+
return this;
275+
}
276+
257277
/**
258278
* Return the database username.
259279
*/
@@ -834,6 +854,7 @@ public DataSourceConfig loadSettings(Properties properties, String poolName) {
834854
private void loadSettings(ConfigPropertiesHelper properties) {
835855
username = properties.get("username", username);
836856
password = properties.get("password", password);
857+
password2 = properties.get("password2", password2);
837858
schema = properties.get("schema", schema);
838859
platform = properties.get("platform", platform);
839860
ownerUsername = properties.get("ownerUsername", ownerUsername);

ebean-datasource/pom.xml

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,35 +22,42 @@
2222
<dependency>
2323
<groupId>io.avaje</groupId>
2424
<artifactId>junit</artifactId>
25-
<version>1.1</version>
25+
<version>1.3</version>
2626
<scope>test</scope>
2727
</dependency>
2828

2929
<dependency>
3030
<groupId>io.ebean</groupId>
3131
<artifactId>ebean-test-containers</artifactId>
32-
<version>6.2</version>
32+
<version>7.1</version>
3333
<scope>test</scope>
3434
</dependency>
3535

3636
<dependency>
3737
<groupId>org.postgresql</groupId>
3838
<artifactId>postgresql</artifactId>
39-
<version>42.5.1</version>
39+
<version>42.6.0</version>
4040
<scope>test</scope>
4141
</dependency>
4242

4343
<dependency>
4444
<groupId>ch.qos.logback</groupId>
4545
<artifactId>logback-classic</artifactId>
46-
<version>1.2.11</version>
46+
<version>1.4.7</version>
47+
<scope>test</scope>
48+
</dependency>
49+
50+
<dependency>
51+
<groupId>org.slf4j</groupId>
52+
<artifactId>slf4j-jdk-platform-logging</artifactId>
53+
<version>2.0.9</version>
4754
<scope>test</scope>
4855
</dependency>
4956

5057
<dependency>
5158
<groupId>io.avaje</groupId>
5259
<artifactId>avaje-slf4j-jpl</artifactId>
53-
<version>1.1</version>
60+
<version>1.2</version>
5461
<scope>test</scope>
5562
</dependency>
5663

ebean-datasource/src/main/java/io/ebean/datasource/pool/ConnectionPool.java

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ final class ConnectionPool implements DataSourcePool {
3333
private final String name;
3434
private final AtomicInteger size = new AtomicInteger(0);
3535
private final DataSourceConfig config;
36+
private final String password2;
3637
/**
3738
* Used to notify of changes to the DataSource status.
3839
*/
@@ -75,6 +76,7 @@ final class ConnectionPool implements DataSourcePool {
7576
private final int waitTimeoutMillis;
7677
private final int pstmtCacheSize;
7778
private final PooledConnectionQueue queue;
79+
private boolean fixedCredentials;
7880
private Timer heartBeatTimer;
7981
/**
8082
* Used to find and close() leaked connections. Leaked connections are
@@ -124,6 +126,8 @@ final class ConnectionPool implements DataSourcePool {
124126
if (pw == null) {
125127
throw new DataSourceConfigurationException("DataSource password is null? url is [" + url + "]");
126128
}
129+
this.password2 = params.getPassword2();
130+
this.fixedCredentials = password2 == null;
127131
this.connectionProps = new Properties();
128132
this.connectionProps.setProperty("user", user);
129133
this.connectionProps.setProperty("password", pw);
@@ -241,7 +245,7 @@ private void initialiseDatabase() throws SQLException {
241245
} catch (SQLException e) {
242246
Log.info("Obtaining connection using ownerUsername:{0} to initialise database", config.getOwnerUsername());
243247
// expected when user does not exist, obtain a connection using owner credentials
244-
try (Connection ownerConnection = createConnection(config.getOwnerUsername(), config.getOwnerPassword())) {
248+
try (Connection ownerConnection = ownerConnection(config.getOwnerUsername(), config.getOwnerPassword())) {
245249
// initialise the DB (typically create the user/role using the owner credentials etc)
246250
InitDatabase initDatabase = config.getInitDatabase();
247251
initDatabase.run(ownerConnection, config);
@@ -454,7 +458,7 @@ private void initConnection(Connection conn) throws SQLException {
454458
/**
455459
* Create an un-pooled connection with the given username and password.
456460
*/
457-
private Connection createConnection(String username, String password) throws SQLException {
461+
private Connection ownerConnection(String username, String password) throws SQLException {
458462
Properties properties = new Properties(connectionProps);
459463
properties.setProperty("user", username);
460464
properties.setProperty("password", password);
@@ -467,7 +471,7 @@ private Connection createConnection() throws SQLException {
467471

468472
private Connection createConnection(Properties properties, boolean notifyIsDown) throws SQLException {
469473
try {
470-
Connection conn = DriverManager.getConnection(url, properties);
474+
final var conn = newConnection(properties);
471475
initConnection(conn);
472476
return conn;
473477
} catch (SQLException ex) {
@@ -478,6 +482,34 @@ private Connection createConnection(Properties properties, boolean notifyIsDown)
478482
}
479483
}
480484

485+
private Connection newConnection(Properties properties) throws SQLException {
486+
try {
487+
return DriverManager.getConnection(url, properties);
488+
} catch (SQLException e) {
489+
notifyLock.lock();
490+
try {
491+
if (fixedCredentials) {
492+
throw e;
493+
}
494+
Log.debug("DataSource [{0}] trying alternate credentials due to {1}", name, e.getMessage());
495+
return switchCredentials(properties);
496+
} finally {
497+
notifyLock.unlock();
498+
}
499+
}
500+
}
501+
502+
private Connection switchCredentials(Properties properties) throws SQLException {
503+
var copy = new Properties(properties);
504+
copy.setProperty("password", password2);
505+
var connection = DriverManager.getConnection(url, copy);
506+
// success, permanently switch to use password2 from now on
507+
Log.info("DataSource [{0}] now using alternate credentials", name);
508+
fixedCredentials = true;
509+
properties.setProperty("password", password2);
510+
return connection;
511+
}
512+
481513
@Override
482514
public void setMaxSize(int max) {
483515
queue.setMaxSize(max);

ebean-datasource/src/test/java/io/ebean/datasource/test/PostgresInitTest.java

Lines changed: 86 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -88,26 +88,8 @@ void test_with_applicationNameAndSchema() throws SQLException {
8888
DataSourcePool pool = DataSourceFactory.create("app", ds);
8989
try {
9090
try (Connection connection = pool.getConnection()) {
91-
try (PreparedStatement statement = connection.prepareStatement("create schema if not exists fred;")) {
92-
statement.execute();
93-
}
94-
connection.commit();
95-
try (PreparedStatement statement = connection.prepareStatement("create table if not exists fred_table (acol integer);")) {
96-
statement.execute();
97-
}
98-
try (PreparedStatement statement = connection.prepareStatement("insert into fred_table (acol) values (?);")) {
99-
statement.setInt(1, 42);
100-
int rows = statement.executeUpdate();
101-
assertThat(rows).isEqualTo(1);
102-
}
103-
try (PreparedStatement statement = connection.prepareStatement("select acol from fred.fred_table")) {
104-
try (ResultSet resultSet = statement.executeQuery()) {
105-
while(resultSet.next()) {
106-
int res = resultSet.getInt(1);
107-
assertThat(res).isEqualTo(42);
108-
}
109-
}
110-
}
91+
setupTable(connection, "my_table");
92+
testConnectionWithSelect(connection, "select acol from app.my_table");
11193
connection.commit();
11294

11395
try (PreparedStatement statement = connection.prepareStatement("select application_name from pg_stat_activity where usename = ?")) {
@@ -124,4 +106,88 @@ void test_with_applicationNameAndSchema() throws SQLException {
124106
pool.shutdown();
125107
}
126108
}
109+
110+
@Test
111+
void test_password2() throws SQLException {
112+
DataSourceConfig ds = new DataSourceConfig();
113+
ds.setUrl("jdbc:postgresql://127.0.0.1:9999/app");
114+
ds.setSchema("fred");
115+
ds.setUsername("db_owner");
116+
ds.setPassword("test");
117+
ds.setPassword2("newRolledPassword");
118+
119+
DataSourcePool pool = DataSourceFactory.create("app", ds);
120+
try {
121+
try (Connection connection0 = pool.getConnection()) {
122+
setupTable(connection0, "my_table2");
123+
testConnectionWithSelect(connection0, "select acol from app.my_table2");
124+
connection0.commit();
125+
try (Connection connection1 = pool.getConnection()) {
126+
testConnectionWithSelect(connection1, "select acol from app.my_table2");
127+
// change password
128+
try (PreparedStatement statement = connection0.prepareStatement("alter role db_owner with password 'newRolledPassword'")) {
129+
statement.execute();
130+
connection0.commit();
131+
}
132+
// existing connections still work
133+
testConnectionWithSelect(connection0, "select acol from app.my_table2");
134+
testConnectionWithSelect(connection1, "select acol from app.my_table2");
135+
136+
// new connection triggers password switch
137+
try (Connection newConnection0 = pool.getConnection()) {
138+
testConnectionWithSelect(newConnection0, "select acol from app.my_table2");
139+
try (Connection newConnection1 = pool.getConnection()) {
140+
testConnectionWithSelect(newConnection1, "select acol from app.my_table2");
141+
}
142+
}
143+
}
144+
145+
// a new pool switches immediately
146+
DataSourcePool pool2 = DataSourceFactory.create("app2", ds);
147+
try (var connP2_0 = pool2.getConnection()) {
148+
testConnectionWithSelect(connP2_0, "select acol from app.my_table2");
149+
try (var connP2_1 = pool2.getConnection()) {
150+
testConnectionWithSelect(connP2_1, "select acol from app.my_table2");
151+
try (var connP2_2 = pool2.getConnection()) {
152+
testConnectionWithSelect(connP2_2, "select acol from app.my_table2");
153+
}
154+
}
155+
}
156+
157+
// reset the password back for other tests
158+
try (PreparedStatement statement = connection0.prepareStatement("alter role db_owner with password 'test'")) {
159+
statement.execute();
160+
connection0.commit();
161+
}
162+
}
163+
} finally {
164+
pool.shutdown();
165+
}
166+
}
167+
168+
169+
private static void setupTable(Connection connection, String tableName) throws SQLException {
170+
try (PreparedStatement statement = connection.prepareStatement("create schema if not exists app;")) {
171+
statement.execute();
172+
}
173+
try (PreparedStatement statement = connection.prepareStatement("create table if not exists app." + tableName + " (acol integer);")) {
174+
statement.execute();
175+
}
176+
try (PreparedStatement statement = connection.prepareStatement("insert into app." + tableName + " (acol) values (?);")) {
177+
statement.setInt(1, 42);
178+
int rows = statement.executeUpdate();
179+
assertThat(rows).isEqualTo(1);
180+
}
181+
}
182+
183+
private static void testConnectionWithSelect(Connection connection, String sql) throws SQLException {
184+
try (PreparedStatement statement = connection.prepareStatement(sql)) {
185+
try (ResultSet resultSet = statement.executeQuery()) {
186+
while (resultSet.next()) {
187+
int res = resultSet.getInt(1);
188+
assertThat(res).isEqualTo(42);
189+
}
190+
}
191+
}
192+
}
127193
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<configuration scan="true" scanPeriod="10 seconds">
2+
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
3+
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
4+
<level>TRACE</level>
5+
</filter>
6+
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
7+
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
8+
</encoder>
9+
</appender>
10+
11+
<root level="INF0">
12+
<appender-ref ref="STDOUT"/>
13+
</root>
14+
15+
<logger name="java.lang" level="WARN"/>
16+
<logger name="io.ebean" level="INFO"/>
17+
<logger name="io.avaje.config" level="TRACE"/>
18+
<logger name="io.ebean.docker" level="DEBUG"/>
19+
<logger name="io.ebean.test" level="TRACE"/>
20+
21+
</configuration>

0 commit comments

Comments
 (0)